kit

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

commit 57e2c48627cc19d3dbce6cb00d2e5af18ad57ca6
parent 856b7e33bbb9fb1cdb96714f784a6836ca82a6ce
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 09:19:13 -0700

opt: fold sxtw/uxtw index into load/store addressing mode at O1 (O1-PATTERNS L8)

Add an extend rider to the optimizer's OPK_INDIRECT index operand
(OptOperand.v.ind.index_ext) and a matching NativeAddr.index_ext, plus a
NativeTarget capability hook can_fold_extend_into_addr (true only for aa64).

When a load/store address's index register is produced by a single-use 32->64
widening convert (CV_SEXT -> sxtw, CV_ZEXT -> uxtw), mir_combine's
try_addr_synth folds the extend into the addressing mode and the aa64 backend
emits [Xbase, Wm, sxtw/uxtw #scale] in one instruction instead of
sxtw; add ...,lsl #scale; [reg].

Gating is by target capability: the recognition pass only stamps the rider
when the consuming backend advertises the hook, so x64/rv64 keep the unfolded
(correct) form and never see a rider they cannot emit. Operand-equality in the
W5 load/compute CSE also compares index_ext so a riderless and ridered indirect
never alias.

aa64 done + validated (signed/unsigned/short/char index, scaled store, negative
index sign-extend; multiply-used + value-reused cases keep the sxtw alive).
x64/rv64 gated off (capability hook absent). test-opt + test-toy 1392/0 green.

Diffstat:
Msrc/arch/aa64/native.c | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/arch/native_target.h | 31++++++++++++++++++++++++++++++-
Msrc/opt/ir.h | 23++++++++++++++++++++++-
Msrc/opt/pass_combine.c | 45+++++++++++++++++++++++++++++++++++++++++++++
Msrc/opt/pass_native_emit.c | 5+++++
5 files changed, 165 insertions(+), 11 deletions(-)

diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -599,13 +599,26 @@ static u32 aa_str_uimm(u32 size, u32 rt, u32 rn, u32 byte_off) { /* Register-offset load/store with an explicit load opcode (ld_opc is consulted * only when load != 0; AA64_LDST_OPC_LDRS_X gives a sign-extending ldrsb/ldrsh - * into the X register). */ + * into the X register) and an explicit index-extend `option` + * (AA64_LDST_OPTION_*): LSL/UXTX (full X index, the default), or SXTW/UXTW for + * a 32-bit W index widened by the addressing mode (O1-PATTERNS L8). */ +static u32 aa_ldst_regoff_opt_v(u32 size, u32 v, u32 load, u32 ld_opc, u32 rt, + u32 rn, u32 rm, u32 option, u32 scaled) { + return aa64_ldst_regoff_pack((AA64LdStRegOff){ + .size = size & 3u, + .V = v & 1u, + .opc = (load ? ld_opc : AA64_LDST_OPC_STR), + .Rm = rm & 0x1fu, + .option = option & 7u, + .S = scaled & 1u, + .Rn = rn & 0x1fu, + .Rt = rt & 0x1fu}); +} + static u32 aa_ldst_regoff_op_v(u32 size, u32 v, u32 load, u32 ld_opc, u32 rt, u32 rn, u32 rm, u32 scaled) { - return ((size & 3u) << 30) | 0x38200800u | ((v & 1u) << 26) | - ((load ? ld_opc : AA64_LDST_OPC_STR) << 22) | ((rm & 0x1fu) << 16) | - (3u << 13) | ((scaled & 1u) << 12) | ((rn & 0x1fu) << 5) | - (rt & 0x1fu); + return aa_ldst_regoff_opt_v(size, v, load, ld_opc, rt, rn, rm, + AA64_LDST_OPTION_LSL, scaled); } static u32 aa_ldst_regoff_v(u32 size, u32 v, u32 load, u32 rt, u32 rn, u32 rm, @@ -1008,8 +1021,16 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, if (addr.index_kind != NATIVE_ADDR_INDEX_NONE) { u32 use_base = base; u32 scaled = 0; + /* L8 index extend: a 32-bit W index widened by the addressing mode. The + * Rm field still names the same register number; only the option bits + * change (010=UXTW, 110=SXTW). NONE keeps the full-width LSL/UXTX form. */ + u32 option = AA64_LDST_OPTION_LSL; if (addr.index_kind != NATIVE_ADDR_INDEX_REG) aa_panic(a, "unsupported address index"); + if (addr.index_ext == NATIVE_ADDR_IDX_EXT_SXTW) + option = AA64_LDST_OPTION_SXTW; + else if (addr.index_ext == NATIVE_ADDR_IDX_EXT_UXTW) + option = AA64_LDST_OPTION_UXTW; if (off) { use_base = AA_TMP1; aa_emit_add_imm(a, use_base, base, off); @@ -1021,8 +1042,9 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, } else { aa_panic(a, "unsupported memory address scale"); } - aa_emit32(mc, aa_ldst_regoff_op_v(sz, native_loc_is_fp(reg), load, ld_opc, - rt, use_base, addr.index.reg, scaled)); + aa_emit32(mc, aa_ldst_regoff_opt_v(sz, native_loc_is_fp(reg), load, ld_opc, + rt, use_base, addr.index.reg, option, + scaled)); return; } if (off >= 0 && (((u32)off & ((1u << sz) - 1u)) == 0) && @@ -1058,11 +1080,27 @@ static int aa_addr_legal(NativeTarget* t, const NativeAddr* addr, if (!addr) return 0; if (addr->index_kind == NATIVE_ADDR_INDEX_NONE) return 1; if (addr->index_kind != NATIVE_ADDR_INDEX_REG) return 0; + /* The SXTW/UXTW index-extend (L8) is encoded in the same regoff form as the + * plain LSL/UXTX index, so the scale legality is identical: scale 0 (no + * shift) or scale == access size (S bit). */ if (addr->log2_scale == 0) return 1; sz = size_idx(mem.size ? mem.size : 8u); return addr->log2_scale == sz; } +/* O1-PATTERNS rider capabilities (see native_target.h). aa64 emits both folded + * forms: the shifted-register ALU op (L7, aa_binop) and the SXTW/UXTW + * index-extend addressing mode (L8, aa_emit_mem). */ +static int aa_can_fold_shift_into_alu(NativeTarget* t) { + (void)t; + return 1; +} + +static int aa_can_fold_extend_into_addr(NativeTarget* t) { + (void)t; + return 1; +} + /* True if `mul Rd, Rn, #c` can be replaced by a single non-mul aarch64 * instruction using only Rn as a source (no extra scratch reg). Constants * that match: 0, 1, -1, +/-2^k, 2^k+1, 1-2^k for k in [1..width-1]. The @@ -1168,8 +1206,22 @@ static void aa_load_addr_from_base(AANativeTarget* a, u32 rd, u32 base, i64 off, /* Read the index before writing rd. O1 can legally select the same physical * register for an address result and its dead-after-use index; materializing * the base into rd first would turn `base + index` into `base + base`. */ - aa_emit32(a->base.mc, - aa_add_lsl(rd, base, addr->index.reg, addr->log2_scale)); + u32 idx_reg = addr->index.reg; + if (addr->index_ext != NATIVE_ADDR_IDX_EXT_NONE) { + /* L8 extend rider on an address-materialization (LEA-shaped) use: the + * shifted-register add takes a full-width X index, so first widen the + * 32-bit W index into a scratch (sxtw/uxtw) before the scaled add. The + * regoff memory form (aa_emit_mem) folds the extend directly; this path is + * the fallback when the indexed indirect is materialized as an address. */ + u32 tmp = aa_tmp_avoiding(rd == base ? base : rd); + if (tmp == base) tmp = aa_tmp_avoiding(base); + if (addr->index_ext == NATIVE_ADDR_IDX_EXT_SXTW) + aa_emit32(a->base.mc, aa_sbfm(1, tmp, idx_reg, 0, 31)); + else + aa_emit32(a->base.mc, aa_ubfm(1, tmp, idx_reg, 0, 31)); + idx_reg = tmp; + } + aa_emit32(a->base.mc, aa_add_lsl(rd, base, idx_reg, addr->log2_scale)); if (off) aa_emit_add_i64(a, rd, rd, off); } @@ -4631,6 +4683,8 @@ NativeTarget* aa64_native_target_new(Compiler* c, ObjBuilder* obj, t->class_for_type = aa_class_for_type; t->imm_legal = aa_imm_legal; t->addr_legal = aa_addr_legal; + t->can_fold_shift_into_alu = aa_can_fold_shift_into_alu; + t->can_fold_extend_into_addr = aa_can_fold_extend_into_addr; t->machine_op_clobbers = aa_machine_op_clobbers; t->func_begin = aa_func_begin; t->func_begin_known_frame = aa_func_begin_known_frame; diff --git a/src/arch/native_target.h b/src/arch/native_target.h @@ -254,13 +254,25 @@ typedef enum NativeImmUse { NATIVE_IMM_ADDR_OFFSET, } NativeImmUse; +/* Addressing-mode index extend (O1-PATTERNS L8). A backend that advertises + * can_fold_extend_into_addr may receive a NATIVE_ADDR_INDEX_REG whose register + * names a 32-bit value the addressing mode widens to 64 bits with this extend + * (aa64 `[Xbase, Wm, sxtw/uxtw #log2_scale]`). NONE = full-width index, the + * default; every other backend only ever sees NONE. */ +typedef enum NativeAddrIndexExt { + NATIVE_ADDR_IDX_EXT_NONE = 0, + NATIVE_ADDR_IDX_EXT_SXTW = 1, + NATIVE_ADDR_IDX_EXT_UXTW = 2, +} NativeAddrIndexExt; + typedef struct NativeAddr { u8 base_kind; /* NativeAddrBaseKind */ u8 cls; /* NativeAllocClass for base value */ u8 index_kind; /* NativeAddrIndexKind */ u8 index_cls; /* NativeAllocClass for index value */ u8 log2_scale; - u8 pad[3]; + u8 index_ext; /* NativeAddrIndexExt */ + u8 pad[2]; KitCgTypeId base_type; KitCgTypeId index_type; union { @@ -448,6 +460,23 @@ struct NativeTarget { NativeAllocClass (*class_for_type)(NativeTarget*, KitCgTypeId); int (*imm_legal)(NativeTarget*, NativeImmUse, u32 op, KitCgTypeId, i64); int (*addr_legal)(NativeTarget*, const NativeAddr*, MemAccess); + /* Optional O1-PATTERNS rider capabilities. The target-agnostic recognition + * passes (pass_combine / pass_addr_fold) only stamp an operand rider after + * the consuming backend advertises the matching capability here, so a + * backend that cannot emit the folded form never receives a rider it would + * have to silently drop. NULL == not supported (the default for every + * backend that has not opted in). + * + * can_fold_shift_into_alu (L7): the backend emits a single-use left shift + * folded into a consuming IADD/ISUB/AND/ORR/EOR as the shifted-register form + * (aa64 `add xD,xA,xS,lsl #k`), honoring OptOperand.shift on a binop's + * second source operand. + * + * can_fold_extend_into_addr (L8): the backend emits a single-use sxtw/uxtw + * of a load/store index folded into the addressing mode (aa64 `[Xb, Wm, + * sxtw/uxtw #scale]`), honoring NativeAddr.index_ext. */ + int (*can_fold_shift_into_alu)(NativeTarget*); + int (*can_fold_extend_into_addr)(NativeTarget*); /* Optional. Report the physical registers the target's encoding of `op` * clobbers as a side effect (not its declared operands/results), one bitmask * per NativeAllocClass. The optimizer keeps values live ACROSS the diff --git a/src/opt/ir.h b/src/opt/ir.h @@ -66,10 +66,27 @@ typedef enum OptOperandKind { } OptOperandKind; #define OPK_REG OPT_OPK_REG +/* Operand riders (O1-PATTERNS L7/L8). Default 0 = no rider, so every existing + * operand and every backend that does not implement the fold keeps behaving as + * before. The recognition passes only stamp a rider after the consuming + * NativeTarget advertises the matching capability hook + * (can_fold_shift_into_alu / can_fold_extend_into_addr), so a backend that + * cannot emit the folded form never sees a rider it would have to drop. */ +typedef enum OptOpIndexExt { + OPT_IDX_EXT_NONE = 0, /* index used at full width (X register) */ + OPT_IDX_EXT_SXTW = 1, /* index is a 32-bit value sign-extended to 64 (W,sxtw) */ + OPT_IDX_EXT_UXTW = 2, /* index is a 32-bit value zero-extended to 64 (W,uxtw) */ +} OptOpIndexExt; + typedef struct OptOperand { u8 kind; u8 cls; - u8 pad[2]; + /* L7 shift rider: when nonzero and kind == OPK_REG, the register value is + * pre-shifted left by `shift` (1..4) before the consuming ALU op uses it, + * emitted as the shifted-register form (`add xD,xA,xS,lsl #shift`). 0 = the + * register is used as-is. Only stamped on a binop's second source operand. */ + u8 shift; + u8 pad[1]; KitCgTypeId type; union { i64 imm; @@ -84,6 +101,10 @@ typedef struct OptOperand { Reg base; Reg index; u8 log2_scale; + /* L8 extend rider (OptOpIndexExt): when SXTW/UXTW, `index` names a 32-bit + * value that the addressing mode widens to 64 bits with the recorded + * extend (`[Xbase, Wm, sxtw #log2_scale]`). NONE = full-width X index. */ + u8 index_ext; i32 ofs; } ind; } v; diff --git a/src/opt/pass_combine.c b/src/opt/pass_combine.c @@ -1186,6 +1186,50 @@ static int try_addr_synth_one_op(CombineCtx* ctx, Inst* in, i32 i, } } + /* (L8) index producer is a single-use 32->64 widening convert (sxtw/uxtw): + * fold the extend into the addressing mode (`[Xbase, Wm, sxtw/uxtw #scale]`), + * one instruction instead of `sxtw xT,wS; add ...,lsl #scale; [reg]`. Runs + * after the ISHL fold above, so an `sxtw; lsl; [reg]` chain first collapses + * the shift into log2_scale and then this absorbs the sxtw producing that + * index. Gated behind the target capability: only a backend that emits the + * extended-register addressing form is handed an index_ext rider. */ + if (op->v.ind.index != (Reg)REG_NONE && + op->v.ind.index_ext == OPT_IDX_EXT_NONE && ctx->target && + ctx->target->can_fold_extend_into_addr && + ctx->target->can_fold_extend_into_addr(ctx->target)) { + Reg idx = op->v.ind.index; + i32 prod_idx = ctx_producer_of(ctx, RC_INT, idx); + if (prod_idx >= 0 && prod_idx < i) { + Inst* prod = &ctx->bl->insts[prod_idx]; + u32 sb = 0, db = 0; + int sign_p = 0; + if (ext_params(prod, &sb, &db, &sign_p) && sb == 4u && db == 8u && + prod->opnds[0].kind == OPK_REG && prod->opnds[0].cls == RC_INT && + prod->opnds[0].v.reg == idx && prod->opnds[1].kind == OPK_REG && + prod->opnds[1].cls == RC_INT) { + Operand prod_def = prod->opnds[0]; + int killed = 0; + int uses_after = count_uses_in_live_range(ctx->f, ctx->bl, prod_idx, + &prod_def, &killed); + /* `<= 2` mirrors the ISHL fold: tolerate the degenerate + * [base == index] aliasing where the convert dst appears twice. The + * convert source must be unchanged since the convert (so the W index + * names the same 32-bit value), and must not alias the convert dst. */ + if (uses_after >= 1 && uses_after <= 2 && + (killed || !opt_block_live_out_has_phys_reg( + ctx->f, ctx->hard_live, ctx->bl->id, &prod_def)) && + !producer_def_aliases_source(&prod_def, &prod->opnds[1]) && + !ctx_def_changed_since(ctx, RC_INT, prod->opnds[1].v.reg, + prod_idx)) { + op->v.ind.index = prod->opnds[1].v.reg; + op->v.ind.index_ext = + sign_p ? (u8)OPT_IDX_EXT_SXTW : (u8)OPT_IDX_EXT_UXTW; + any = 1; + } + } + } + } + return any; } @@ -1698,6 +1742,7 @@ static int same_load_addr_operand(const Operand* a, const Operand* b) { return a->v.ind.base == b->v.ind.base && a->v.ind.index == b->v.ind.index && a->v.ind.log2_scale == b->v.ind.log2_scale && + a->v.ind.index_ext == b->v.ind.index_ext && a->v.ind.ofs == b->v.ind.ofs; default: return 0; diff --git a/src/opt/pass_native_emit.c b/src/opt/pass_native_emit.c @@ -312,6 +312,11 @@ static NativeAddr addr_from_operand(NativeEmitCtx* e, const OptOperand* op, addr.index_cls = NATIVE_REG_INT; addr.index.reg = op->v.ind.index; addr.log2_scale = op->v.ind.log2_scale; + /* L8 extend rider: OPT_IDX_EXT_* and NATIVE_ADDR_IDX_EXT_* share the + * 0=NONE/1=SXTW/2=UXTW encoding. Only set by the addr-fold recognition + * when the target advertised can_fold_extend_into_addr, so a backend + * without the capability always sees NONE. */ + addr.index_ext = op->v.ind.index_ext; addr.offset = op->v.ind.ofs; return addr; case OPT_OPK_REG: