kit

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

commit bf084c97cb134114880b7f0c936697e14cfb33da
parent 7087a3c382b08472a57ec07f33498c2b300e41f8
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 09:48:57 -0700

merge: O1-PATTERNS.md §2 linear -O1 opt items L1–L10 (o1p/integration)

Diffstat:
Mmk/test.mk | 16+++++++++++++++-
Msrc/arch/aa64/native.c | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/arch/native_target.h | 41++++++++++++++++++++++++++++++++++++++---
Msrc/opt/ir.h | 23++++++++++++++++++++++-
Msrc/opt/pass_combine.c | 406++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/opt/pass_native_emit.c | 11+++++++++++
Atest/opt/o1p_combine.sh | 403+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/opt/o1p_rider.sh | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 1225 insertions(+), 15 deletions(-)

diff --git a/mk/test.mk b/mk/test.mk @@ -865,7 +865,7 @@ test-macho: lib $(TEST_RT_DEP) $(ROUNDTRIP_BIN_MACHO) $(LINK_EXE_RUNNER) $(JIT_R OPT_TEST_BIN = build/test/cg_ir_lower_test TINY_INLINE_TEST_BIN = build/test/tiny_inline_test -test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail test-opt-x64-win-tail-sret test-opt-prologue-tier test-opt-whole-program-inline test-opt-lto-phase1 test-opt-redundant-copy-ext test-opt-redundant-frame-sub test-opt-o1-branch-cleanup test-opt-hot-slot-order test-opt-o1-remat test-opt-aa64-x29-bottom test-opt-o1-coalesce test-opt-o1-switch-imm test-opt-o1-inline-cap test-opt-rv64-far-slot test-opt-o1-cmp-imm test-opt-o1-stack-dse test-opt-o1-local-cse test-opt-o1p-aa64 test-opt-o1p-const-divmul +test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail test-opt-x64-win-tail-sret test-opt-prologue-tier test-opt-whole-program-inline test-opt-lto-phase1 test-opt-redundant-copy-ext test-opt-redundant-frame-sub test-opt-o1-branch-cleanup test-opt-hot-slot-order test-opt-o1-remat test-opt-aa64-x29-bottom test-opt-o1-coalesce test-opt-o1-switch-imm test-opt-o1-inline-cap test-opt-rv64-far-slot test-opt-o1-cmp-imm test-opt-o1-stack-dse test-opt-o1-local-cse test-opt-o1p-aa64 test-opt-o1p-const-divmul test-opt-o1p-combine test-opt-o1p-rider $(OPT_TEST_BIN) @@ -961,6 +961,20 @@ test-opt-o1p-aa64: bin test-opt-o1p-const-divmul: bin @KIT=$(abspath $(BIN)) bash test/opt/o1p_const_divmul.sh +# Structural+behavioral check: L1 store->load forwarding, L6 sxt-after-load drop, +# L2 cmp;cset;cbnz->cmp_branch fusion, L4 double-cset drop, L5 copy coalescing +# (O1-PATTERNS L1/L2/L4/L5/L6). Red-green degrades to green-only without KIT_BASE. +.PHONY: test-opt-o1p-combine +test-opt-o1p-combine: bin + @KIT=$(abspath $(BIN)) bash test/opt/o1p_combine.sh + +# Structural+correctness check: L7 folds a single-use shift into the ALU op +# (add xD,xB,xS,lsl#k); L8 folds a sxtw/uxtw index into the addressing mode +# ([Xb,Wm,sxtw#s]); aa64 only, multiply-used not folded (O1-PATTERNS L7/L8). +.PHONY: test-opt-o1p-rider +test-opt-o1p-rider: bin + @KIT=$(abspath $(BIN)) bash test/opt/o1p_rider.sh + test-opt-tiny-inline: bin $(TINY_INLINE_TEST_BIN) $(TINY_INLINE_TEST_BIN) 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, @@ -769,6 +782,32 @@ static u32 aa_add_lsl(u32 rd, u32 rn, u32 rm, u32 shift) { .Rd = rd}); } +/* L7 shifted-register ALU forms: rd = rn <op> (rm << shift). shift type is + * always LSL (the .shift field, 0); the shift amount is imm6. add/sub use the + * add/sub shifted-register family, and/orr/eor the logical shifted-register + * family. `sf` selects the 32- vs 64-bit operand width. */ +static u32 aa_addsub_lsl(u32 sf, u32 op, u32 rd, u32 rn, u32 rm, u32 shift) { + return aa64_addsubsr_pack((AA64AddSubSR){.sf = sf, + .op = op, + .S = 0, + .shift = 0, + .Rm = rm, + .imm6 = shift, + .Rn = rn, + .Rd = rd}); +} + +static u32 aa_logsr_lsl(u32 sf, u32 opc, u32 rd, u32 rn, u32 rm, u32 shift) { + return aa64_logsr_pack((AA64LogSR){.sf = sf, + .opc = opc, + .shift = 0, + .N = 0, + .Rm = rm, + .imm6 = shift, + .Rn = rn, + .Rd = rd}); +} + static u32 aa_cset(u32 sf, u32 rd, u32 cond) { return aa64_csinc_enc(sf, rd, AA64_ZR, AA64_ZR, cond ^ 1u); } @@ -1008,8 +1047,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 +1068,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 +1106,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 +1232,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); } @@ -2856,6 +2934,33 @@ static void aa_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc lhs, aa_emit32(t->mc, aa64_eor_imm(sf, rd, rn, N, immr, imms)); return; } + /* L7 shifted-register ALU: a single-use `lsl rm,#k` (k in 1..4) folded into + * this op's second source. Emit `<op> rd,rn,rm,lsl #k` in one instruction + * instead of `lsl rT,rm,#k; <op> rd,rn,rT`. Only an integer register rhs can + * carry a shift rider (immediates never do, and the recognition pass restricts + * it to add/sub/and/orr/eor). */ + if (rhs.kind == NATIVE_LOC_REG && rhs.shift) { + u32 k = rhs.shift; + switch (op) { + case BO_IADD: + aa_emit32(t->mc, aa_addsub_lsl(sf, /*op=add*/ 0u, rd, rn, rm, k)); + return; + case BO_ISUB: + aa_emit32(t->mc, aa_addsub_lsl(sf, /*op=sub*/ 1u, rd, rn, rm, k)); + return; + case BO_AND: + aa_emit32(t->mc, aa_logsr_lsl(sf, AA64_LOG_AND_OPC, rd, rn, rm, k)); + return; + case BO_OR: + aa_emit32(t->mc, aa_logsr_lsl(sf, AA64_LOG_ORR_OPC, rd, rn, rm, k)); + return; + case BO_XOR: + aa_emit32(t->mc, aa_logsr_lsl(sf, AA64_LOG_EOR_OPC, rd, rn, rm, k)); + return; + default: + aa_panic(aa_of(t), "shift rider on unsupported binop"); + } + } switch (op) { case BO_IADD: aa_emit32(t->mc, aa64_add(sf, rd, rn, rm)); @@ -4631,6 +4736,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 { @@ -294,7 +306,13 @@ typedef struct NativeLoc { * producers); the arch then falls back to the live cg_type_size query, so * partial adoption stays byte-identical. */ u8 szinfo; - u8 pad[1]; + /* L7 shift rider (O1-PATTERNS): when nonzero on a register NativeLoc passed + * as the second source of a binop, the value is pre-shifted left by `shift` + * (1..4) and the backend emits the shifted-register ALU form + * (`add xD,xA,xS,lsl #shift`). 0 = use the register as-is. Only stamped when + * the target advertised can_fold_shift_into_alu, so a backend without the + * capability never sees it. */ + u8 shift; KitCgTypeId type; union { Reg reg; @@ -448,6 +466,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 @@ -717,7 +752,7 @@ static inline NativeLoc native_loc_from_reg(NativeRegLoc r) { loc.kind = (u8)(r.is_imm ? NATIVE_LOC_IMM : NATIVE_LOC_REG); loc.cls = r.cls; loc.szinfo = r.szinfo; - loc.pad[0] = 0; + loc.shift = 0; /* O1-only L7 rider; the -O0 NDT fast path never sets it */ loc.type = r.type; if (r.is_imm) loc.v.imm = r.v.imm; 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; } @@ -1202,6 +1246,84 @@ static int try_addr_synth(CombineCtx* ctx, Inst* in, i32 i) { return any; } +/* ---- L7: fold a single-use left shift into the consuming ALU op ---- */ + +/* True for the integer binops aa64 can emit as a shifted-register form + * (`<op> rd,rn,rm,lsl #k`). ISUB is included: it is non-commutative but the + * shifted operand is always the SECOND source (the subtrahend), which is + * exactly where the rider sits. IMUL/div/etc. have no shifted-register form. */ +static int binop_takes_shifted_rhs(BinOp op) { + switch (op) { + case BO_IADD: + case BO_ISUB: + case BO_AND: + case BO_OR: + case BO_XOR: + return 1; + default: + return 0; + } +} + +/* L7 recognition. When `in` is one of the shifted-register-capable binops and + * its second source register is produced by a single-use `IR_BINOP SHL reg,imm` + * (shift 1..4) at the SAME operand width, fold the shift into the binop by + * rewriting opnds[2] to the shift's source register and stamping the shift + * rider. The aa64 backend then emits the one-instruction shifted form; the dead + * SHL is retired by mir_dce. Gated by the target capability so only a backend + * that emits the shifted form ever receives a rider. */ +static int try_fold_shift_into_alu(CombineCtx* ctx, Inst* in, i32 i) { + if ((IROp)in->op != IR_BINOP || in->nopnds != 3) return 0; + if (!binop_takes_shifted_rhs((BinOp)in->extra.imm)) return 0; + Operand* rhs = &in->opnds[2]; + if (rhs->kind != OPK_REG || rhs->cls != RC_INT || rhs->shift != 0) return 0; + if (!ctx->target || !ctx->target->can_fold_shift_into_alu || + !ctx->target->can_fold_shift_into_alu(ctx->target)) + return 0; + + i32 prod_idx = ctx_producer_of(ctx, RC_INT, rhs->v.reg); + if (prod_idx < 0 || prod_idx >= i) return 0; + Inst* prod = &ctx->bl->insts[prod_idx]; + if ((IROp)prod->op != IR_BINOP || prod->nopnds != 3 || + (BinOp)prod->extra.imm != BO_SHL || prod->opnds[0].kind != OPK_REG || + prod->opnds[0].cls != RC_INT || prod->opnds[0].v.reg != rhs->v.reg || + prod->opnds[1].kind != OPK_REG || prod->opnds[1].cls != RC_INT || + prod->opnds[2].kind != OPK_IMM) + return 0; + i64 sh = prod->opnds[2].v.imm; + if (sh < 1 || sh > 4) return 0; + + /* Width safety: the shifted-register operand shifts the rm register at the + * CONSUMING binop's width. If the SHL produced a narrower value (e.g. a + * 32-bit shift) than the binop reads (64-bit), the high bits of the source + * register are not the shift's, so the inline shift would be wrong. Require + * the shift's result width to equal the binop's result width. */ + u32 shl_w = combine_scalar_width_bytes(ctx->f, prod->opnds[0].type); + u32 alu_w = combine_scalar_width_bytes(ctx->f, in->opnds[0].type); + if (!shl_w || !alu_w || shl_w != alu_w) return 0; + + 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); + /* The shift def must be single-use (it dies at this binop); folding while + * other uses survive would leave the SHL live AND add a redundant inline + * shift. The source must be unchanged since the SHL and must not alias the + * SHL dst (else rewriting the operand to the source changes its value). */ + if (uses_after != 1) return 0; + if (!killed && opt_block_live_out_has_phys_reg(ctx->f, ctx->hard_live, + ctx->bl->id, &prod_def)) + return 0; + if (producer_def_aliases_source(&prod_def, &prod->opnds[1])) return 0; + if (ctx_def_changed_since(ctx, RC_INT, prod->opnds[1].v.reg, prod_idx)) + return 0; + + rhs->v.reg = prod->opnds[1].v.reg; + rhs->shift = (u8)sh; + ctx->block_change_p = 1; + return 1; +} + /* ---- Rewrite 3: sink producer into single-use IR_COPY destination ---- */ static int try_sink(CombineCtx* ctx, Inst* in, i32 i) { @@ -1315,6 +1437,41 @@ static int try_combine_exts(CombineCtx* ctx, Inst* in, i32 i) { return 1; } + /* L6 (O1-PATTERNS): the SIGNED mirror of the ZEXT-of-load fold above. A + * narrow integer load fills the ENTIRE destination register on every target + * (native_direct_target.c:1700): a flagged sign-extending load (MF_SEXT_LOAD, + * `ldrsb`/`ldrsh`) sign-extends to register width, and a plain load + * (`ldrb`/`ldrh`) zero-extends to register width. A subsequent SXTB/SXTH/SXTW + * convert is then a no-op in two shapes — drop it to an IR_COPY (copy-prop + + * DCE retire it; worst case a same-cost register move): + * + * (a) SEXT of a sign-extending load of equal-or-smaller width + * (`mem.size <= sb`): the load already replicated the loaded sign bit + * across the whole register, so re-sign-extending from `sb >= mem.size` + * bytes re-reads bits that are already that sign extension -> identity. + * (`ldrsb x; sxtb w` / `ldrsb x; sxtw x` — yyjson's signature.) + * + * (b) SEXT of a ZERO-extending load whose value cannot have the sign bit set + * in the widened position (`mem.size < sb`, STRICT): the loaded value + * occupies only the low `mem.size` bytes and bit `8*sb-1` is in the + * zeroed region, so the sign bit the SEXT replicates is guaranteed 0 -> + * the SEXT equals the ZEXT the load already produced -> identity. Strict + * `<` is required: at `mem.size == sb` the load's top byte may set the + * sign bit, and SEXT would then differ from the zero-extended value. + * (`ldrb w; sxtw x` of a 0..255 byte — lvm/sqlite.) */ + if (outer_sign && src_cls == RC_INT && (IROp)prod->op == IR_LOAD && + prod->extra.mem.size && prod->nopnds >= 1 && + same_reg_operand(&prod->opnds[0], &in->opnds[1])) { + int sext_load = (prod->extra.mem.flags & MF_SEXT_LOAD) != 0; + if ((sext_load && prod->extra.mem.size <= sb) || + (!sext_load && prod->extra.mem.size < sb)) { + in->op = IR_COPY; + in->nopnds = 2; /* opnds[0]=dst, opnds[1]=src already in place */ + ctx->block_change_p = 1; + return 1; + } + } + u32 isb, idb; int inner_sign; if (!ext_params(prod, &isb, &idb, &inner_sign)) return 0; @@ -1370,6 +1527,211 @@ static int try_combine_exts(CombineCtx* ctx, Inst* in, i32 i) { return 0; } +/* Invert a CmpOp (negate the relation). Returns 0 for an op outside the + * known total set. Kept byte-for-byte in sync with src/cg/fold.c + * api_invert_cmp and pass_jump.c invert_cmp — including the FP rule that + * negation flips ordered<->unordered (so the NaN outcome flips too): the + * negation of ordered `a<b` is *unordered* `a>=b`. */ +static int combine_invert_cmp(CmpOp op, CmpOp* out) { + switch (op) { + case CMP_EQ: *out = CMP_NE; return 1; + case CMP_NE: *out = CMP_EQ; return 1; + case CMP_LT_S: *out = CMP_GE_S; return 1; + case CMP_LE_S: *out = CMP_GT_S; return 1; + case CMP_GT_S: *out = CMP_LE_S; return 1; + case CMP_GE_S: *out = CMP_LT_S; return 1; + case CMP_LT_U: *out = CMP_GE_U; return 1; + case CMP_LE_U: *out = CMP_GT_U; return 1; + case CMP_GT_U: *out = CMP_LE_U; return 1; + case CMP_GE_U: *out = CMP_LT_U; return 1; + case CMP_OEQ_F: *out = CMP_UNE_F; return 1; + case CMP_ONE_F: *out = CMP_UEQ_F; return 1; + case CMP_OLT_F: *out = CMP_UGE_F; return 1; + case CMP_OLE_F: *out = CMP_UGT_F; return 1; + case CMP_OGT_F: *out = CMP_ULE_F; return 1; + case CMP_OGE_F: *out = CMP_ULT_F; return 1; + case CMP_UEQ_F: *out = CMP_ONE_F; return 1; + case CMP_UNE_F: *out = CMP_OEQ_F; return 1; + case CMP_ULT_F: *out = CMP_OGE_F; return 1; + case CMP_ULE_F: *out = CMP_OGT_F; return 1; + case CMP_UGT_F: *out = CMP_OLE_F; return 1; + case CMP_UGE_F: *out = CMP_OLT_F; return 1; + } + return 0; +} + +/* ---- L4 (O1-PATTERNS): drop the double-cset bool re-normalize ---- + * + * kit lowers `(a cc b)` to an IR_CMP producing a 0/1 bool in rInner, then a + * surrounding `bool != 0` / `if (bool)` context lowers to a SECOND IR_CMP + * `rOuter = (rInner <eq/ne> 0|1)` that re-normalizes an already-0/1 value + * (`cmp w8,#1; cset w8,eq; cmp w8,#0; cset w8,ne` — yyjson's 573-triple + * signature). The outer compare is redundant. When an IR_CMP's source register + * (opnds[1]) has its most-recent same-block def being another IR_CMP (so a + * proven 0/1 bool) and the outer compares it against imm 0 or 1 with eq/ne, + * rewrite the outer in place: + * + * (t != 0) / (t == 1): outer == inner -> IR_COPY rOuter, rInner + * (t == 0) / (t != 1): outer == !inner -> recompute the inner relation + * inverted on the inner's OWN operands: + * IR_CMP rOuter = (inner.a invert(inner.cc) inner.b) + * + * The copy form lets copy-prop + DCE retire it; the inverted form drops the + * dependency on rInner (so the inner cmp dies if single-use) and replaces two + * cmp+cset pairs with one. Both are same-block forward peepholes off the + * CombineCtx last-def map. Composes with L2: once the outer is a copy/inverted + * cmp, a branch consuming rOuter folds through copy-prop / the L2 fusion on a + * later fixpoint iteration. */ +static int try_drop_double_cmp(CombineCtx* ctx, Inst* in, i32 i) { + if ((IROp)in->op != IR_CMP || in->nopnds < 3) return 0; + if (in->opnds[0].kind != OPK_REG || in->opnds[1].kind != OPK_REG) return 0; + if (in->opnds[2].kind != OPK_IMM) return 0; + CmpOp outer = (CmpOp)in->extra.imm; + if (outer != CMP_EQ && outer != CMP_NE) return 0; + i64 k = in->opnds[2].v.imm; + if (k != 0 && k != 1) return 0; + + Operand srcb = in->opnds[1]; + i32 prod_idx = ctx_producer_of(ctx, srcb.cls, srcb.v.reg); + if (prod_idx < 0 || prod_idx >= i) return 0; + Inst* inner = &ctx->bl->insts[prod_idx]; + if ((IROp)inner->op != IR_CMP || inner->nopnds < 3) return 0; + if (inner->opnds[0].kind != OPK_REG || + !same_phys_reg(&inner->opnds[0], &srcb)) + return 0; + /* The outer dst and the inner bool live in the same register class (both are + * 0/1 integer bools); a class crossing would need a real move, not a copy. */ + if (in->opnds[0].cls != inner->opnds[0].cls) return 0; + + /* Whether the outer result equals the inner bool or its negation: + * (t != 0) -> t ; (t == 1) -> t (non-inverted) + * (t == 0) -> !t; (t != 1) -> !t (inverted) */ + int invert = (outer == CMP_EQ && k == 0) || (outer == CMP_NE && k == 1); + + if (!invert) { + /* outer == inner: rewrite to a copy off rInner (copy-prop + DCE retire). */ + in->op = (u16)IR_COPY; + in->opnds[1] = inner->opnds[0]; /* = srcb, already in place; keep explicit */ + in->nopnds = 2; + in->extra.imm = 0; + ctx->block_change_p = 1; + return 1; + } + + /* Inverted: recompute the inner relation negated on the inner's own operands. + * That requires the inner's operands to still hold their pre-cmp values: an + * operand must not have been redefined since the inner cmp AND must not alias + * the inner cmp's own destination (a self-referencing `cmp w8,#1` writing w8 + * leaves w8 holding the bool, not the compared value — recomputing from it + * would be wrong). */ + CmpOp inv; + if (!combine_invert_cmp((CmpOp)inner->extra.imm, &inv)) return 0; + for (u32 oi = 1; oi < 3; ++oi) { + const Operand* p = &inner->opnds[oi]; + if (p->kind == OPK_REG) { + if (same_phys_reg(p, &inner->opnds[0])) return 0; + if (ctx_def_changed_since(ctx, p->cls, p->v.reg, prod_idx)) return 0; + } + } + /* Keep the outer's dst (opnds[0]); replace its inputs with the inner's and + * its CmpOp with the inverted inner relation. The dst type/class is the same + * bool the outer already produced. */ + in->opnds[1] = inner->opnds[1]; + in->opnds[2] = inner->opnds[2]; + in->extra.imm = (i64)inv; + ctx->block_change_p = 1; + return 1; +} + +/* ---- L2 (O1-PATTERNS): fuse cmp rD; cmp_branch(NE/EQ, rD, #0) -> cmp_branch + * + * A relational whose 0/1 bool was MATERIALIZED into a register (`cmp; cset + * rD,cc`) and then re-tested by the branch (`cbnz/cbz rD`) lowers, at this + * MIR level, to `IR_CMP rD = (a cc b)` followed by the terminator + * `IR_CMP_BRANCH(CMP_NE|CMP_EQ, rD, #0)` (control.c synthesizes cmp_branch of + * the bool against IMM_ZERO; cg's delayed-compare path already fuses the + * un-materialized `if (a<b)` form upstream, so only the materialized residual + * reaches here). The branch's NE/EQ-vs-0 just re-tests a bool the original cmp + * already computed in flags. + * + * When the terminator is `IR_CMP_BRANCH(NE|EQ, rD, #0)` whose rD's most-recent + * same-block def is a single-use IR_CMP, fuse into the cmp's own relation: + * cbnz (CMP_NE vs 0): branch-if-true -> cmp_branch(cmp.op, a, b) + * cbz (CMP_EQ vs 0): branch-if-false -> cmp_branch(invert(cmp.op), a, b) + * and NOP the now-dead cmp. This is the inverse of the SSA fusion in + * pass_o2.c (ssa_combine_fold_cmp_branch), here over the no-SSA CombineCtx + * (last-def map + hard-live single-use). Inversion uses the full CmpOp table + * (combine_invert_cmp), which is correct for FP too: the cset collapsed the + * comparison to a definite 0/1 even for NaN, and the inverse op reproduces the + * same NaN branch direction (e.g. !(a OEQ b) == a UNE b). + * + * Single-block, single forward pass, all guards from existing CombineCtx state + * -> linear. */ +static int try_fuse_cmp_branch(CombineCtx* ctx, Inst* in, i32 i) { + if ((IROp)in->op != IR_CMP_BRANCH || in->nopnds < 2) return 0; + /* Only the block terminator carries the two CFG successors. */ + if (i != (i32)ctx->bl->ninsts - 1 || ctx->bl->nsucc < 2) return 0; + /* Branch must test a register bool against immediate 0 with EQ/NE. */ + CmpOp brop = (CmpOp)in->extra.imm; + if (brop != CMP_EQ && brop != CMP_NE) return 0; + if (in->opnds[0].kind != OPK_REG) return 0; + if (in->opnds[1].kind != OPK_IMM || in->opnds[1].v.imm != 0) return 0; + + Operand cond = in->opnds[0]; + i32 prod_idx = ctx_producer_of(ctx, cond.cls, cond.v.reg); + if (prod_idx < 0 || prod_idx >= i) return 0; + Inst* cmp = &ctx->bl->insts[prod_idx]; + if ((IROp)cmp->op != IR_CMP || cmp->nopnds < 3) return 0; + if (cmp->opnds[0].kind != OPK_REG || !same_phys_reg(&cmp->opnds[0], &cond)) + return 0; + + /* Resolve the fused relation (invert for the cbz / EQ-vs-0 case). */ + CmpOp fused = (CmpOp)cmp->extra.imm; + if (brop == CMP_EQ && !combine_invert_cmp(fused, &fused)) return 0; + + /* The cmp's input operands must be unchanged between the cmp and the branch + * (an intervening inst may have redefined a register the cmp reads). */ + for (u32 oi = 1; oi < 3; ++oi) { + const Operand* p = &cmp->opnds[oi]; + if (p->kind == OPK_REG && + ctx_def_changed_since(ctx, p->cls, p->v.reg, prod_idx)) + return 0; + } + + /* The cmp's result must die at this branch: its sole use, not live-out. If it + * has other uses (or escapes the block) the cmp must stay and we cannot NOP + * it. */ + Operand cmp_def = cmp->opnds[0]; + int killed = 0; + if (count_uses_in_live_range(ctx->f, ctx->bl, prod_idx, &cmp_def, &killed) != + 1) + return 0; + if (!killed && opt_block_live_out_has_phys_reg(ctx->f, ctx->hard_live, + ctx->bl->id, &cmp_def)) + return 0; + + /* Fuse: rewrite the branch to test the cmp's own relation directly; NOP the + * cmp. Successors are unchanged (succ[0]=taken, succ[1]=fallthrough). */ + Operand* opnds = arena_array(ctx->f->arena, Operand, 2); + opnds[0] = cmp->opnds[1]; + opnds[1] = cmp->opnds[2]; + in->opnds = opnds; + in->nopnds = 2; + in->extra.imm = (i64)fused; + + cmp->op = (u16)IR_NOP; + cmp->def = VAL_NONE; + cmp->ndefs = 0; + cmp->defs = NULL; + cmp->nopnds = 0; + cmp->opnds = NULL; + /* The cmp no longer defines cond's register; restore its prior reaching def + * so later same-block availability checks stay accurate. */ + ctx_restore_removed_def(ctx, &cond, prod_idx); + ctx->block_change_p = 1; + return 1; +} + /* ---- Rewrite 6 (W1a): local frame-address `sub`-CSE ---- * * O1.md W1a. An `IR_ADDR_OF rD, <addr>` materializes a frame-slot or global @@ -1458,6 +1820,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; @@ -1589,7 +1952,10 @@ static int same_compute_operand(const Operand* a, const Operand* b) { if (a->kind != b->kind) return 0; switch (a->kind) { case OPK_REG: - return a->cls == b->cls && a->v.reg == b->v.reg; + /* The L7 shift rider is part of the operand's value: `x2` and `x2,lsl#2` + * are different inputs, so a ridered and a riderless operand must not be + * treated as the same compute (else CSE would drop the shift). */ + return a->cls == b->cls && a->v.reg == b->v.reg && a->shift == b->shift; case OPK_IMM: return a->v.imm == b->v.imm && a->type == b->type; default: @@ -1882,8 +2248,21 @@ static int opt_combine_fold_block(Func* f, Block* bl, try_addr_of_cse(&ctx, in, i); try_fold_const_convert(&ctx, in, i); try_combine_exts(&ctx, in, i); + /* L4: collapse a double-cset bool re-normalize (outer IR_CMP of an inner + * 0/1 bool against 0/1) before L2/substitution see the outer's consumer. */ + try_drop_double_cmp(&ctx, in, i); + /* L2: fuse a same-block cmp feeding the IR_CMP_BRANCH terminator into a + * direct relational branch (run before try_cmp_imm_fold so the cmp's + * immediate slot still folds on the fused branch). */ + try_fuse_cmp_branch(&ctx, in, i); try_substitute(&ctx, in, i); try_addr_synth(&ctx, in, i); + /* L7: fold a single-use left shift into this binop's shifted-register + * form. Run after substitution (so a copy-propagated rhs is resolved) + * and after addr-synth (address shifts go into the EA, not the ALU + * rider), and before the compute-CSE record below so the ridered operand + * shape is what gets recorded. */ + try_fold_shift_into_alu(&ctx, in, i); /* W5: same-block redundant-load + pure-compute reuse. Run after * addr-synth so both the recorded entries and this lookup see the * canonical (post-fold) address/operand shapes. Each turns the redundant @@ -1937,6 +2316,31 @@ static int opt_combine_compact_block(Func* f, Block* bl) { changed = 1; continue; } + /* L1 (O1-PATTERNS): store->load forwarding across a register mismatch. + * `str rX,[slot]; ldr rY,[slot]` (rX != rY, same slot+size) reloads a + * value the just-stored register rX still holds. The store and load are + * adjacent in the COMPACTED stream (prev is the immediately-preceding + * kept inst; only NOPs — which clobber nothing — may have sat between + * them), so rX is unchanged and the slot is unwritten between the two: + * the exact no-clobber adjacency the same-reg case above relies on. + * Rewrite the reload in place to `copy rY <- rX`; the next fold iteration + * copy-propagates rX into rY's uses (often deleting the copy), and once + * rX is dead with the slot unread, W8 stack-DSE + mir_dce retire the + * store. Guard: only when the stored value is a register of the load + * dst's class (an immediate/odd-class store is left as a real reload). */ + if ((IROp)prev->op == IR_STORE && (IROp)in->op == IR_LOAD && + same_spill_slot_and_size(f, prev, in) && prev->nopnds >= 2 && + in->nopnds >= 2 && prev->opnds[1].kind == OPK_REG && + in->opnds[0].kind == OPK_REG && + prev->opnds[1].cls == in->opnds[0].cls) { + in->op = (u16)IR_COPY; + in->opnds[1] = prev->opnds[1]; + in->nopnds = 2; + memset(&in->extra, 0, sizeof in->extra); + changed = 1; + bl->insts[w++] = *in; + continue; + } if ((IROp)prev->op == IR_LOAD && (IROp)in->op == IR_STORE && same_spill_slot_and_size(f, prev, in) && same_reg_operand(&prev->opnds[0], &in->opnds[1])) { 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: @@ -943,6 +948,12 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, dst_reg, loc_avoid_reg(b), in->loc); b = operand_imm_or_reg(e, &in->opnds[2], NATIVE_IMM_BINOP, (u32)in->extra.imm, a.v.reg, dst_reg, in->loc); + /* L7 shift rider: a single-use SHL folded into this binop's second + * source. The rider only survives onto a register `b` (a shifted-reg + * ALU operand); an immediate `b` never carries one. The recognition + * pass only stamps it for targets advertising can_fold_shift_into_alu, + * so a backend ignoring NativeLoc.shift never receives a nonzero rider. */ + if (b.kind == NATIVE_LOC_REG) b.shift = in->opnds[2].shift; if (dst.kind != NATIVE_LOC_REG) dst = scratch_loc(e, in->opnds[0].type, class_for_type(e, in->opnds[0].type), a.v.reg, diff --git a/test/opt/o1p_combine.sh b/test/opt/o1p_combine.sh @@ -0,0 +1,403 @@ +#!/usr/bin/env bash +# Structural + correctness guards for the five -O1 same-block MIR peepholes from +# doc/plan/O1-PATTERNS.md §2 (L1, L6, L2, L4, L5), all implemented in +# src/opt/pass_combine.c on top of the existing per-block CombineCtx machinery. +# +# Each L-item has its own clearly-labeled section below. Every section is a +# RED-GREEN check: the same fixture is compiled with the candidate kit ($KIT) +# AND with a saved baseline ($KIT_BASE, default build/kit_base). The guard +# asserts the target idiom is PRESENT in the baseline ("red") and REDUCED / +# GONE in the candidate ("green"), and that the program still computes the +# identical, correct result at -O0 and -O1. +# +# Items L1 and L6 use the exact ecosystem files the patterns were mined from +# (lvm.c for L1's spill-reload churn, yyjson.c for L6's sxtb-after-ldrsb); if +# those sources are not provisioned in the kit ecosystem cache the structural +# half of that item is SKIPPED (the harness prints SKIP) while the synthetic +# correctness half still runs. L2/L4/L5 use self-contained synthetic fixtures. +set -uo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +KIT="${KIT:-$ROOT/build/kit}" +KIT_BASE="${KIT_BASE:-$ROOT/build/kit_base}" +WORK="$ROOT/build/test/opt/o1p_combine" +mkdir -p "$WORK" +SYS="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || echo)" + +fail() { + printf 'o1p_combine FAILED: %s\n' "$1" >&2 + shift || true + for f in "$@"; do + printf ' --- %s ---\n' "$f" >&2 + sed 's/^/ | /' "$f" >&2 + done + exit 1 +} + +have_base=1 +[ -x "$KIT_BASE" ] || { have_base=0; printf 'o1p_combine: note: no %s — red-green checks degrade to green-only\n' "$KIT_BASE" >&2; } + +fn_body() { # $1 = file, $2 = symbol -> stdout body + awk -v want="$2" ' + $0 ~ ("^[0-9a-f]+ <_?" want ">:") { in_fn = 1; next } + /^[0-9a-f]+ </ { in_fn = 0 } + in_fn { print } + ' "$1" +} + +eco_src() { "$ROOT/scripts/ecosystem.sh" srcdir "$1" 2>/dev/null; } + +# ============================================================================ +# L1 — store->load forwarding across a register mismatch +# ============================================================================ +# `str rX,[slot]; ldr rY,[slot]` (rX != rY, same slot+size) reloads a value rX +# still holds. opt_combine_compact_block now rewrites the reload to `copy rY,rX` +# (copy-prop + W8 DSE + mir_dce then retire it). The signature shape lives in +# lvm.c's luaV_execute; we count adjacent same-slot diff-register str;ldr pairs. +echo "== L1 store->load forwarding ==" +count_strldr() { # $1 = disasm file -> stdout count of adjacent diff-reg str;ldr +python3 - "$1" <<'PY' +import re, sys +lines = open(sys.argv[1]).read().splitlines() +srx = re.compile(r'\b(str)\s+([wx]\d+),\s*\[(x29|sp),\s*(#[0-9]+|#-?0x[0-9a-f]+)?\]') +lrx = re.compile(r'\b(ldr)\s+([wx]\d+),\s*\[(x29|sp),\s*(#[0-9]+|#-?0x[0-9a-f]+)?\]') +def p(line, rx): + m = rx.search(line) + return (m.group(2), m.group(3), m.group(4) or '#0') if m else None +n = 0 +for i in range(len(lines)-1): + a, b = p(lines[i], srx), p(lines[i+1], lrx) + if a and b and a[1] == b[1] and a[2] == b[2] and a[0] != b[0]: + n += 1 +print(n) +PY +} +LVM_SRC="$(eco_src lua)" +if [ -n "$LVM_SRC" ] && [ -f "$LVM_SRC/lvm.c" ] && [ -n "$SYS" ]; then + "$KIT" cc -O1 -I"$LVM_SRC" --sysroot "$SYS" -c "$LVM_SRC/lvm.c" -o "$WORK/lvm.cand.o" \ + > "$WORK/lvm.cand.cc" 2>&1 || fail "L1 lvm candidate compile failed" "$WORK/lvm.cand.cc" + "$KIT" objdump -d "$WORK/lvm.cand.o" > "$WORK/lvm.cand.dis" 2>&1 + CAND_N="$(count_strldr "$WORK/lvm.cand.dis")" + if [ "$have_base" = 1 ]; then + "$KIT_BASE" cc -O1 -I"$LVM_SRC" --sysroot "$SYS" -c "$LVM_SRC/lvm.c" -o "$WORK/lvm.base.o" \ + > "$WORK/lvm.base.cc" 2>&1 || fail "L1 lvm baseline compile failed" "$WORK/lvm.base.cc" + "$KIT_BASE" objdump -d "$WORK/lvm.base.o" > "$WORK/lvm.base.dis" 2>&1 + BASE_N="$(count_strldr "$WORK/lvm.base.dis")" + [ "$BASE_N" -ge 100 ] || fail "L1 red precondition: expected baseline lvm to have many str;ldr-diff-reg (got $BASE_N)" + # Green: candidate must eliminate the large majority of them. + [ "$CAND_N" -le $((BASE_N / 4)) ] || \ + fail "L1: candidate did not forward enough str;ldr (base=$BASE_N cand=$CAND_N)" + printf ' L1: lvm str;ldr-diff-reg base=%s -> cand=%s (RED->GREEN)\n' "$BASE_N" "$CAND_N" + else + [ "$CAND_N" -le 100 ] || fail "L1: candidate lvm still has $CAND_N str;ldr-diff-reg" + printf ' L1: lvm str;ldr-diff-reg cand=%s (green-only)\n' "$CAND_N" + fi +else + printf ' L1: SKIP structural (lua/lvm.c not provisioned or no SDK)\n' +fi + +# L1 correctness: spill-heavy program, identical result -O0 vs -O1. +cat > "$WORK/l1run.c" <<'EOF' +extern long ext(long); +static long acc = 0; +long ext(long x) { acc += x; return (x * 2654435761u) ^ (x >> 3); } +/* Many simultaneously-live values forced through the frame, each re-read after + * the homing stores — the L1 forwarding shape. */ +static long spilly(long a, long b, long c, long d, long e, long f, long g, long h) { + long v0=a*3+1, v1=b*5+2, v2=c*7+3, v3=d*11+4; + long v4=e*13+5, v5=f*17+6, v6=g*19+7, v7=h*23+8; + long s = ext(v0 + v1); + s += ext(v2 + v3); s += ext(v4 + v5); s += ext(v6 + v7); + return s + v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7; +} +int main(void) { + long r = 0; + for (long i = 0; i < 37; i++) { + acc = 0; + r ^= spilly(i, i+1, i+2, i+3, i+4, i+5, i+6, i+7); + r ^= acc; + } + return (int)(r & 0x7f); +} +EOF +"$KIT" cc -O0 -std=c11 "$WORK/l1run.c" -o "$WORK/l1run.o0" >"$WORK/l1run.o0.cc" 2>&1 || fail "L1 run -O0 compile" "$WORK/l1run.o0.cc" +"$KIT" cc -O1 -std=c11 "$WORK/l1run.c" -o "$WORK/l1run.o1" >"$WORK/l1run.o1.cc" 2>&1 || fail "L1 run -O1 compile" "$WORK/l1run.o1.cc" +"$WORK/l1run.o0"; L1_O0=$? +"$WORK/l1run.o1"; L1_O1=$? +[ "$L1_O0" = "$L1_O1" ] || fail "L1 run O0/O1 differ: $L1_O0 vs $L1_O1" +printf ' L1: run rc=%s (O0==O1)\n' "$L1_O0" + +# ============================================================================ +# L6 — drop redundant sxtb/sxth/sxtw after a same-width-or-wider extending load +# ============================================================================ +# combine_exts now drops a SEXT whose source's most-recent same-block def is a +# sign-extending load of <= width, OR a zero-extending narrow load whose value +# cannot have the sign bit set (mem.size < sb). Signature lives in yyjson.c +# (`ldrsb x; sxtb w`). We count `sxtb` (yyjson has no sxth and its sxtw aren't +# load-rooted). +echo "== L6 drop redundant sxt after extending load ==" +YY_SRC="$(eco_src yyjson)" +if [ -n "$YY_SRC" ] && [ -f "$YY_SRC/yyjson.c" ] && [ -n "$SYS" ]; then + "$KIT" cc -O1 -I"$YY_SRC" --sysroot "$SYS" -c "$YY_SRC/yyjson.c" -o "$WORK/yy.cand.o" \ + > "$WORK/yy.cand.cc" 2>&1 || fail "L6 yyjson candidate compile failed" "$WORK/yy.cand.cc" + "$KIT" objdump -d "$WORK/yy.cand.o" > "$WORK/yy.cand.dis" 2>&1 + CAND_SXTB="$(grep -cE '\bsxtb\b' "$WORK/yy.cand.dis" || true)" + if [ "$have_base" = 1 ]; then + "$KIT_BASE" cc -O1 -I"$YY_SRC" --sysroot "$SYS" -c "$YY_SRC/yyjson.c" -o "$WORK/yy.base.o" \ + > "$WORK/yy.base.cc" 2>&1 || fail "L6 yyjson baseline compile failed" "$WORK/yy.base.cc" + "$KIT_BASE" objdump -d "$WORK/yy.base.o" > "$WORK/yy.base.dis" 2>&1 + BASE_SXTB="$(grep -cE '\bsxtb\b' "$WORK/yy.base.dis" || true)" + [ "$BASE_SXTB" -ge 50 ] || fail "L6 red precondition: expected baseline yyjson many sxtb (got $BASE_SXTB)" + [ "$CAND_SXTB" -lt "$BASE_SXTB" ] || fail "L6: candidate did not drop any sxtb (base=$BASE_SXTB cand=$CAND_SXTB)" + printf ' L6: yyjson sxtb base=%s -> cand=%s (RED->GREEN)\n' "$BASE_SXTB" "$CAND_SXTB" + else + printf ' L6: yyjson sxtb cand=%s (green-only)\n' "$CAND_SXTB" + fi +else + printf ' L6: SKIP structural (yyjson.c not provisioned or no SDK)\n' +fi + +# L6 correctness: signed-char + zero-extending-byte widening, O0 vs O1. +cat > "$WORK/l6run.c" <<'EOF' +/* Exercises both L6 shapes: signed-byte loads widened (ldrsb;sxtb / sxtw) and + * unsigned-byte loads widened to long (ldrb;sxtw, sign bit provably 0). */ +static long sgn(const signed char *s, int n) { + long acc = 0; + for (int i = 0; i < n; i++) { + signed char c = s[i]; /* sign-extending load */ + int e = (int)c; /* sxtb -> no-op */ + long w = (long)c; /* sxtw of the sign-extended byte -> no-op */ + acc += (long)e + w + (c < 0 ? 1 : 0); + } + return acc; +} +static long usn(const unsigned char *s, int n) { + long acc = 0; + for (int i = 0; i < n; i++) { + unsigned char c = s[i]; /* zero-extending load */ + long w = (long)(int)c; /* widen a 0..255 value: sign bit provably 0 */ + acc += w * 3; + } + return acc; +} +int main(void) { + signed char sc[256]; + unsigned char uc[256]; + for (int i = 0; i < 256; i++) { sc[i] = (signed char)(i - 128); uc[i] = (unsigned char)i; } + long r = sgn(sc, 256) ^ usn(uc, 256); + return (int)(r & 0x7f); +} +EOF +"$KIT" cc -O0 -std=c11 "$WORK/l6run.c" -o "$WORK/l6run.o0" >"$WORK/l6run.o0.cc" 2>&1 || fail "L6 run -O0 compile" "$WORK/l6run.o0.cc" +"$KIT" cc -O1 -std=c11 "$WORK/l6run.c" -o "$WORK/l6run.o1" >"$WORK/l6run.o1.cc" 2>&1 || fail "L6 run -O1 compile" "$WORK/l6run.o1.cc" +"$WORK/l6run.o0"; L6_O0=$? +"$WORK/l6run.o1"; L6_O1=$? +[ "$L6_O0" = "$L6_O1" ] || fail "L6 run O0/O1 differ: $L6_O0 vs $L6_O1" +printf ' L6: run rc=%s (O0==O1)\n' "$L6_O0" + +# ============================================================================ +# L2 — fuse cmp rD; cmp_branch(NE/EQ, rD, #0) -> cmp_branch (boolean-into-branch) +# ============================================================================ +# When a relational's 0/1 bool was materialized into a register (`cset rD`) and +# then re-tested by the terminator (`cbnz/cbz rD`), and the cmp + branch land in +# the SAME MIR block (single-use cmp), try_fuse_cmp_branch rewrites the branch to +# test the cmp's own relation and NOPs the cmp. (Many `if`s split the cmp and +# branch into different blocks at no-SSA O1 — those are out of reach for a +# same-block peephole; L2 captures the co-located subset, e.g. inlined +# predicates in yyjson.) Structural signal: fewer `cset` on yyjson. +echo "== L2 cmp;cset;cbnz/cbz -> cmp_branch fusion ==" +if [ -n "${YY_SRC:-}" ] && [ -f "$YY_SRC/yyjson.c" ] && [ -n "$SYS" ]; then + # candidate yy.cand.dis was produced by the L6 section above. + CAND_CSET="$(grep -cE '\bcset\b' "$WORK/yy.cand.dis" || true)" + if [ "$have_base" = 1 ]; then + BASE_CSET="$(grep -cE '\bcset\b' "$WORK/yy.base.dis" || true)" + [ "$BASE_CSET" -ge 100 ] || fail "L2 red precondition: expected baseline yyjson many cset (got $BASE_CSET)" + [ "$CAND_CSET" -lt "$BASE_CSET" ] || fail "L2: candidate did not reduce cset (base=$BASE_CSET cand=$CAND_CSET)" + printf ' L2: yyjson cset base=%s -> cand=%s (RED->GREEN; L1/L6/L2 cumulative)\n' "$BASE_CSET" "$CAND_CSET" + else + printf ' L2: yyjson cset cand=%s (green-only)\n' "$CAND_CSET" + fi +else + printf ' L2: SKIP structural (yyjson.c not provisioned or no SDK)\n' +fi + +# L2 correctness — the critical trap is mis-inverting the condition. Exercise +# every relation in BOTH branch polarities (if(t) and if(!t)) plus FP NaN/inf, +# forcing materialization, and require kit -O0 == kit -O1. +cat > "$WORK/l2run.c" <<'EOF' +static int hits = 0; +static void use(int x) { hits += x; } +static long f(int a, int b, double x, double y) { + int lt=(a<b); if(lt)use(1); if(!lt)use(2); + int le=(a<=b); if(le)use(4); if(!le)use(8); + int gt=(a>b); if(gt)use(16); if(!gt)use(32); + int ge=(a>=b); if(ge)use(64); if(!ge)use(128); + int eq=(a==b); if(eq)use(256); if(!eq)use(512); + int ne=(a!=b); if(ne)use(1024); if(!ne)use(2048); + unsigned ua=(unsigned)a, ub=(unsigned)b; + int ult=(ua<ub); if(ult)use(4096); if(!ult)use(8192); + int uge=(ua>=ub); if(uge)use(16384); if(!uge)use(32768); + int feq=(x==y); if(feq)use(1<<16); if(!feq)use(1<<17); + int flt=(x<y); if(flt)use(1<<18); if(!flt)use(1<<19); + return hits; +} +int main(void) { + long acc = 0; + double ds[5] = {-1.0, 0.0, 1.0, 2.0, 3.0/(double)(0.0==1.0 ? 1.0 : 1e308*1e308)}; + for (int a=-3;a<=3;a++) for (int b=-3;b<=3;b++) + for (int i=0;i<5;i++) for (int j=0;j<5;j++) { hits=0; acc=acc*131+f(a,b,ds[i],ds[j]); } + return (int)(acc & 0x7f); +} +EOF +"$KIT" cc -O0 -std=c11 "$WORK/l2run.c" -o "$WORK/l2run.o0" >"$WORK/l2run.o0.cc" 2>&1 || fail "L2 run -O0 compile" "$WORK/l2run.o0.cc" +"$KIT" cc -O1 -std=c11 "$WORK/l2run.c" -o "$WORK/l2run.o1" >"$WORK/l2run.o1.cc" 2>&1 || fail "L2 run -O1 compile" "$WORK/l2run.o1.cc" +"$WORK/l2run.o0"; L2_O0=$? +"$WORK/l2run.o1"; L2_O1=$? +[ "$L2_O0" = "$L2_O1" ] || fail "L2 run O0/O1 differ (mis-inverted condition?): $L2_O0 vs $L2_O1" +printf ' L2: run rc=%s (O0==O1; all relations x both polarities x FP)\n' "$L2_O0" + +# ============================================================================ +# L4 — drop the double-cset re-normalize (cset rD; cmp rD,#0|#1; cset rD) +# ============================================================================ +# An IR_CMP `rOuter = (rInner <eq/ne> 0|1)` whose rInner is itself an IR_CMP +# (a proven 0/1 bool) re-normalizes an already-0/1 value. try_drop_double_cmp +# rewrites the outer to a COPY of rInner (t!=0 / t==1) or to the inner relation +# INVERTED on the inner's own operands (t==0 / t!=1; skipped when an inner +# operand aliases the inner dst). Signature: yyjson's 573 `cset;cmp#0/1;cset` +# triples. +echo "== L4 drop double-cset re-normalize ==" +count_triple() { # $1 = disasm file -> stdout count of cset;cmp#0|#1;cset triples +python3 - "$1" <<'PY' +import re, sys +L = open(sys.argv[1]).read().splitlines() +cset = re.compile(r'\bcset\s+([wx]\d+),') +cmp01 = re.compile(r'\bcmp\s+([wx]\d+),\s*#[01]\b') +n = 0 +for i in range(len(L)-2): + if cset.search(L[i]) and cmp01.search(L[i+1]) and cset.search(L[i+2]): + n += 1 +print(n) +PY +} +if [ -n "${YY_SRC:-}" ] && [ -f "$YY_SRC/yyjson.c" ] && [ -n "$SYS" ]; then + CAND_TRIP="$(count_triple "$WORK/yy.cand.dis")" + if [ "$have_base" = 1 ]; then + BASE_TRIP="$(count_triple "$WORK/yy.base.dis")" + [ "$BASE_TRIP" -ge 100 ] || fail "L4 red precondition: expected baseline yyjson many double-cset triples (got $BASE_TRIP)" + [ "$CAND_TRIP" -le $((BASE_TRIP / 2)) ] || fail "L4: candidate did not drop enough triples (base=$BASE_TRIP cand=$CAND_TRIP)" + printf ' L4: yyjson cset;cmp#0/1;cset triples base=%s -> cand=%s (RED->GREEN)\n' "$BASE_TRIP" "$CAND_TRIP" + else + [ "$CAND_TRIP" -le 100 ] || fail "L4: candidate yyjson still has $CAND_TRIP triples" + printf ' L4: yyjson triples cand=%s (green-only)\n' "$CAND_TRIP" + fi +else + printf ' L4: SKIP structural (yyjson.c not provisioned or no SDK)\n' +fi + +# L4 correctness: double/triple bool normalization in both directions, O0==O1. +cat > "$WORK/l4run.c" <<'EOF' +static int use_count = 0; +static void use(int x) { use_count += x; } +/* Force the cset;cmp;cset re-normalize: a relational bool fed into further + * !=0 / ==0 / double-negation boolean contexts. */ +static long f(int a, int b) { + int t = (a < b); + int u = (t != 0); /* copy form */ + int v = !(a == b); /* invert form */ + int w = !!(a > b); /* double-not */ + int x = ((a <= b) == 0); /* invert via ==0 */ + if (u) use(1); + if (v) use(2); + if (w) use(4); + if (x) use(8); + if (((a >= b) != 1)) use(16); + unsigned ua=(unsigned)a, ub=(unsigned)b; + int y = !(ua < ub); + if (y) use(32); + return use_count + u + v + w + x + y; +} +int main(void) { + long acc = 0; + for (int a=-4;a<=4;a++) for (int b=-4;b<=4;b++) { use_count=0; acc=acc*131+f(a,b); } + return (int)(acc & 0x7f); +} +EOF +"$KIT" cc -O0 -std=c11 "$WORK/l4run.c" -o "$WORK/l4run.o0" >"$WORK/l4run.o0.cc" 2>&1 || fail "L4 run -O0 compile" "$WORK/l4run.o0.cc" +"$KIT" cc -O1 -std=c11 "$WORK/l4run.c" -o "$WORK/l4run.o1" >"$WORK/l4run.o1.cc" 2>&1 || fail "L4 run -O1 compile" "$WORK/l4run.o1.cc" +"$WORK/l4run.o0"; L4_O0=$? +"$WORK/l4run.o1"; L4_O1=$? +[ "$L4_O0" = "$L4_O1" ] || fail "L4 run O0/O1 differ (mis-normalized bool?): $L4_O0 vs $L4_O1" +printf ' L4: run rc=%s (O0==O1; copy + invert + double-not forms)\n' "$L4_O0" + +# ============================================================================ +# L5 — single-use register copy coalesced into its consumer +# ============================================================================ +# STATUS: the L5 transformation (forward a single-use copy's source into its +# convert/extend, store-value, call-arg, and IR_RET-value consumers) is already +# implemented by the existing same-block copy propagator try_substitute +# (unconditional SK_REG forwarding into every whitelisted consumer slot, +# pass_combine.c) plus the IR_RET back-propagation try_ret_retarget. The only +# residual is forwarding an EMIT-SCRATCH-sourced copy, which is unsound at MIR +# level (native emit repurposes the scratch regs to materialize locals BETWEEN +# instructions — disabling the scratch-source guard miscompiles sqlite), and the +# broad reg-to-reg `mov` surplus the catalog counted is ABI-mandated arg-shuffle/ +# save moves + the cross-block register-residency problem (NEEDS-SSA-O2, the +# catalog's own §3). So this section is a COVERAGE/REGRESSION guard: it asserts +# the spec's named consumer coalescing holds (no redundant single-use copy +# survives feeding a convert / store-value / return) rather than introducing a +# new, unsound fold. +echo "== L5 single-use copy coalescing (coverage/regression) ==" +cat > "$WORK/l5cov.c" <<'EOF' +extern void usel(long); +/* copy -> convert/extend consumer: the value should be widened directly. */ +long conv_c(const unsigned char *p, int i) { + unsigned char b = p[i]; + long w = (long)(int)b; /* no `mov; sxt` of an intermediate */ + return w; +} +/* copy -> store-value consumer. */ +void store_c(int *q, int v) { int t = v + 1; q[2] = t; } +/* copy -> return consumer (value not from the immediately-preceding inst). */ +long ret_c(long a, long b) { long t = a * b; long u = t; usel(0); return u; } +EOF +"$KIT" cc -O1 -std=c11 -c "$WORK/l5cov.c" -o "$WORK/l5cov.o" >"$WORK/l5cov.cc" 2>&1 || fail "L5 cov compile" "$WORK/l5cov.cc" +"$KIT" objdump -d "$WORK/l5cov.o" > "$WORK/l5cov.dis" 2>&1 +CONV_BODY="$(fn_body "$WORK/l5cov.dis" conv_c)" +STORE_BODY="$(fn_body "$WORK/l5cov.dis" store_c)" +# conv_c: no reg-to-reg mov should feed the widen (the load result widens directly). +CONV_MOV="$(printf '%s\n' "$CONV_BODY" | grep -cE '\bmov\s+[wx][0-9]+, [wx][0-9]+\b' || true)" +[ "$CONV_MOV" -eq 0 ] || fail "L5: conv_c has a redundant reg-to-reg mov ($CONV_MOV)" "$WORK/l5cov.dis" +# store_c: the store value reads the computed result directly (no mov before str). +STORE_MOVSTORE="$(printf '%s\n' "$STORE_BODY" | grep -E -A1 '\bmov\s+[wx][0-9]+, [wx][0-9]+\b' | grep -cE '\bstr[bh]?\b' || true)" +[ "$STORE_MOVSTORE" -eq 0 ] || fail "L5: store_c keeps a mov feeding the store ($STORE_MOVSTORE)" "$WORK/l5cov.dis" +printf ' L5: convert/store/return copies coalesced (conv mov=%s, store mov;str=%s) [subsumed by try_substitute/try_ret_retarget]\n' "$CONV_MOV" "$STORE_MOVSTORE" + +# L5 correctness: copy-chain heavy program, O0 vs O1. +cat > "$WORK/l5run.c" <<'EOF' +extern long sinkl(long); +static long acc = 0; +long sinkl(long x) { acc += x; return x ^ 0x5a5a5a5a; } +static long chain(long a, long b, long c) { + long x = a; + long y = x; /* copy chain */ + long z = y + b; + long w = z; /* copy */ + long t = (int)(char)w; /* copy -> convert */ + long r = sinkl(t + c); /* copy -> call arg */ + long u = r; /* copy -> return */ + return u; +} +int main(void) { + long s = 0; + for (long i=-50;i<=50;i++) { acc=0; s = s*131 + chain(i, i*2+1, i*3-7) + acc; } + return (int)(s & 0x7f); +} +EOF +"$KIT" cc -O0 -std=c11 "$WORK/l5run.c" -o "$WORK/l5run.o0" >"$WORK/l5run.o0.cc" 2>&1 || fail "L5 run -O0 compile" "$WORK/l5run.o0.cc" +"$KIT" cc -O1 -std=c11 "$WORK/l5run.c" -o "$WORK/l5run.o1" >"$WORK/l5run.o1.cc" 2>&1 || fail "L5 run -O1 compile" "$WORK/l5run.o1.cc" +"$WORK/l5run.o0"; L5_O0=$? +"$WORK/l5run.o1"; L5_O1=$? +[ "$L5_O0" = "$L5_O1" ] || fail "L5 run O0/O1 differ: $L5_O0 vs $L5_O1" +printf ' L5: run rc=%s (O0==O1; copy-chain into convert/call/return)\n' "$L5_O0" + +echo "o1p_combine: OK" diff --git a/test/opt/o1p_rider.sh b/test/opt/o1p_rider.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# Structural + correctness guards for the two -O1 operand-rider folds from +# doc/plan/O1-PATTERNS.md §2 (L7, L8). Both extend the optimizer Operand model +# with a rider field consumed only by a backend that advertises the matching +# NativeTarget capability hook (aa64 today; x64/rv64 keep the unfolded form): +# +# L7 shift rider on a register ALU operand +# a + (b<<k) -> add xD,xB,xS,lsl #k (no separate `lsl`) +# src/opt/pass_combine.c try_fold_shift_into_alu + aa64 aa_binop +# +# L8 extend rider on a load/store index operand +# a[i] (signed int i) -> ldr wD,[xB, wI, sxtw #2] (no sxtw+add) +# src/opt/pass_combine.c try_addr_synth (L8 block) + aa64 aa_emit_mem +# +# Each section is GREEN-only against the candidate kit ($KIT): it asserts the +# folded idiom is PRESENT and the unfolded scaffolding (separate lsl / sxtw+add) +# is GONE, that a multiply-used producer is NOT folded (negative case), and that +# the program computes the identical correct result at -O0 and -O1. When a +# baseline ($KIT_BASE) is present the structural assertions are additionally +# checked RED on it (the baseline must still emit the unfolded form). The folded +# forms are aa64-specific, so the structural disasm checks only run on an +# arm64/Darwin host; correctness runs wherever the host can execute the output. +set -uo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +KIT="${KIT:-$ROOT/build/kit}" +KIT_BASE="${KIT_BASE:-$ROOT/build/kit_base}" +WORK="$ROOT/build/test/opt/o1p_rider" +mkdir -p "$WORK" +SYS="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || echo)" + +# Structural disasm checks only make sense for the aa64 folded forms; gate them +# on an arm64 host (the only place this worktree natively runs the output too). +HOST_ARCH="$(uname -m 2>/dev/null || echo unknown)" +do_struct=1 +case "$HOST_ARCH" in + arm64 | aarch64) ;; + *) do_struct=0 ;; +esac + +fail() { + printf 'o1p_rider FAILED: %s\n' "$1" >&2 + shift || true + for f in "$@"; do + printf ' --- %s ---\n' "$f" >&2 + sed 's/^/ | /' "$f" >&2 + done + exit 1 +} + +have_base=1 +[ -x "$KIT_BASE" ] || { + have_base=0 + printf 'o1p_rider: note: no %s — RED baseline checks skipped (GREEN-only)\n' \ + "$KIT_BASE" >&2 +} + +# count_re FILE REGEX -> number of matching disasm lines +count_re() { grep -cE "$2" "$1" || true; } + +# ---------------------------------------------------------------------------- +# L7 — fold a single-use shift into the consuming ALU op +# ---------------------------------------------------------------------------- +echo "== L7 shift-into-ALU ==" +cat > "$WORK/l7.c" <<'EOF' +long add_shl(long a, long b) { return a + (b << 3); } +long sub_shl(long a, long b) { return a - (b << 2); } +long and_shl(long a, long b) { return a & (b << 1); } +EOF +cat > "$WORK/l7_neg.c" <<'EOF' +extern long g; +/* shift result used twice (stored AND added): must NOT fold; lsl stays. */ +long multi_use(long a, long b) { long s = b << 3; g = s; return a + s; } +EOF + +if [ "$do_struct" = 1 ] && [ -n "$SYS" ]; then + "$KIT" cc -O1 --sysroot "$SYS" -c "$WORK/l7.c" -o "$WORK/l7.o" \ + > "$WORK/l7.cc" 2>&1 || fail "L7 candidate compile failed" "$WORK/l7.cc" + "$KIT" objdump -d "$WORK/l7.o" > "$WORK/l7.dis" 2>&1 + # GREEN: at least three shifted-register ALU ops (add/sub/and ...,lsl #k), + # and zero standalone `lsl xN,xM,#k` left behind. + SR="$(count_re "$WORK/l7.dis" '\b(add|sub|and|orr|eor)\b[^;]*,[[:space:]]*lsl[[:space:]]+#[1-4]\b')" + # A STANDALONE lsl is the instruction mnemonic (`lsl xN,xM,#k`), distinct from + # the `lsl #k` shift modifier inside a shifted-register ALU op. Match the + # mnemonic position (after the tab, immediately followed by a register). + LSL="$(count_re "$WORK/l7.dis" $'\tlsl[[:space:]]+[wx][0-9]+,')" + [ "$SR" -ge 3 ] || fail "L7: expected >=3 shifted-reg ALU ops, got $SR" "$WORK/l7.dis" + [ "$LSL" -eq 0 ] || fail "L7: standalone lsl should be folded away, got $LSL" "$WORK/l7.dis" + printf ' L7: shifted-reg ALU ops=%s, standalone lsl=%s (GREEN)\n' "$SR" "$LSL" + + # Negative: multiply-used shift keeps its lsl and a plain add. + "$KIT" cc -O1 --sysroot "$SYS" -c "$WORK/l7_neg.c" -o "$WORK/l7n.o" \ + > "$WORK/l7n.cc" 2>&1 || fail "L7 neg compile failed" "$WORK/l7n.cc" + "$KIT" objdump -d "$WORK/l7n.o" > "$WORK/l7n.dis" 2>&1 + NEG_LSL="$(count_re "$WORK/l7n.dis" $'\tlsl[[:space:]]+[wx][0-9]+,')" + NEG_SR="$(count_re "$WORK/l7n.dis" '\badd\b[^;]*,[[:space:]]*lsl[[:space:]]+#[1-4]\b')" + [ "$NEG_LSL" -ge 1 ] || fail "L7 neg: multiply-used lsl must remain, got $NEG_LSL" "$WORK/l7n.dis" + [ "$NEG_SR" -eq 0 ] || fail "L7 neg: must NOT fold a multiply-used shift, got $NEG_SR" "$WORK/l7n.dis" + printf ' L7 neg: multiply-used lsl kept=%s, folded=%s (correctly NOT folded)\n' \ + "$NEG_LSL" "$NEG_SR" + + if [ "$have_base" = 1 ]; then + "$KIT_BASE" cc -O1 --sysroot "$SYS" -c "$WORK/l7.c" -o "$WORK/l7.base.o" \ + > "$WORK/l7.base.cc" 2>&1 || fail "L7 baseline compile failed" "$WORK/l7.base.cc" + "$KIT_BASE" objdump -d "$WORK/l7.base.o" > "$WORK/l7.base.dis" 2>&1 + BSR="$(count_re "$WORK/l7.base.dis" '\b(add|sub|and|orr|eor)\b[^;]*,[[:space:]]*lsl[[:space:]]+#[1-4]\b')" + [ "$BSR" -eq 0 ] || fail "L7 RED precondition: baseline already folds (sr=$BSR)" "$WORK/l7.base.dis" + printf ' L7: baseline shifted-reg ALU ops=%s (RED) -> candidate=%s (GREEN)\n' "$BSR" "$SR" + fi +else + printf ' L7: structural disasm checks skipped (non-aa64 host or no SDK)\n' +fi + +# ---------------------------------------------------------------------------- +# L8 — fold a sxtw/uxtw index into the load/store addressing mode +# ---------------------------------------------------------------------------- +echo "== L8 extend-into-addr ==" +cat > "$WORK/l8.c" <<'EOF' +int load_signed(int* a, int i) { return a[i]; } +void store_signed(long* a, int i, long v) { a[i] = v; } +unsigned load_unsigned(unsigned* a, unsigned ui) { return a[ui]; } +EOF +cat > "$WORK/l8_neg.c" <<'EOF' +extern long g; +/* widened index used twice (stored AND used as index): the sxtw must remain. */ +long shared(long* a, int i) { long w = (long)i; g = w; return a[w]; } +EOF + +if [ "$do_struct" = 1 ] && [ -n "$SYS" ]; then + "$KIT" cc -O1 --sysroot "$SYS" -c "$WORK/l8.c" -o "$WORK/l8.o" \ + > "$WORK/l8.cc" 2>&1 || fail "L8 candidate compile failed" "$WORK/l8.cc" + "$KIT" objdump -d "$WORK/l8.o" > "$WORK/l8.dis" 2>&1 + # GREEN: the load/store carry the folded extended-register index, and there + # is no standalone `sxtw`/`uxtw` left to widen it. + FOLD="$(count_re "$WORK/l8.dis" '\b(ldr|str)\b[^;]*\[[^]]*,[[:space:]]*w[0-9]+,[[:space:]]*(sxtw|uxtw)[[:space:]]+#[0-3]\]')" + # Standalone sxtw/uxtw is the mnemonic `sxtw xN,wM` — distinct from the + # `sxtw #k` extend modifier inside a folded addressing mode. + STX="$(count_re "$WORK/l8.dis" $'\t(sxtw|uxtw)[[:space:]]+[wx][0-9]+,')" + [ "$FOLD" -ge 2 ] || fail "L8: expected >=2 folded sxtw/uxtw addressing modes, got $FOLD" "$WORK/l8.dis" + [ "$STX" -eq 0 ] || fail "L8: standalone sxtw/uxtw should be folded away, got $STX" "$WORK/l8.dis" + printf ' L8: folded extend addressing modes=%s, standalone sxtw/uxtw=%s (GREEN)\n' "$FOLD" "$STX" + + # Negative: a widened value reused beyond the index keeps its sxtw. + "$KIT" cc -O1 --sysroot "$SYS" -c "$WORK/l8_neg.c" -o "$WORK/l8n.o" \ + > "$WORK/l8n.cc" 2>&1 || fail "L8 neg compile failed" "$WORK/l8n.cc" + "$KIT" objdump -d "$WORK/l8n.o" > "$WORK/l8n.dis" 2>&1 + NEG_SXTW="$(count_re "$WORK/l8n.dis" $'\tsxtw[[:space:]]+x[0-9]+,')" + [ "$NEG_SXTW" -ge 1 ] || fail "L8 neg: reused sxtw must remain, got $NEG_SXTW" "$WORK/l8n.dis" + printf ' L8 neg: reused sxtw kept=%s (the stored widened value stays live)\n' "$NEG_SXTW" + + if [ "$have_base" = 1 ]; then + "$KIT_BASE" cc -O1 --sysroot "$SYS" -c "$WORK/l8.c" -o "$WORK/l8.base.o" \ + > "$WORK/l8.base.cc" 2>&1 || fail "L8 baseline compile failed" "$WORK/l8.base.cc" + "$KIT_BASE" objdump -d "$WORK/l8.base.o" > "$WORK/l8.base.dis" 2>&1 + BFOLD="$(count_re "$WORK/l8.base.dis" '\b(ldr|str)\b[^;]*\[[^]]*,[[:space:]]*w[0-9]+,[[:space:]]*(sxtw|uxtw)[[:space:]]+#[0-3]\]')" + [ "$BFOLD" -eq 0 ] || fail "L8 RED precondition: baseline already folds (fold=$BFOLD)" "$WORK/l8.base.dis" + printf ' L8: baseline folded addressing modes=%s (RED) -> candidate=%s (GREEN)\n' "$BFOLD" "$FOLD" + fi +else + printf ' L8: structural disasm checks skipped (non-aa64 host or no SDK)\n' +fi + +# ---------------------------------------------------------------------------- +# Correctness: O0 must equal O1, exercising both folds (incl. negative index +# sign-extension and the multiply-used / value-reused non-fold paths). +# ---------------------------------------------------------------------------- +echo "== rider correctness (O0 == O1) ==" +cat > "$WORK/run.c" <<'EOF' +long g; +long add_shl(long a, long b) { return a + (b << 3); } +long sub_shl(long a, long b) { return a - (b << 2); } +long multi_use(long a, long b) { long s = b << 3; g = s; return a + s; } +long load_neg(long* a, int i) { return a[i]; } /* sxtw: i may be negative */ +unsigned load_u(unsigned* a, unsigned ui) { return a[ui]; } +long shared(long* a, int i) { long w = (long)i; g = w; return a[w]; } +int sum(int* a, int n) { int s = 0; for (int i = 0; i < n; i++) s += a[i] + (i << 2); return s; } + +int main(void) { + long la[5] = {7, 8, 9, 11, 13}; + unsigned ua[5] = {100, 200, 300, 400, 500}; + int ia[6] = {1, 2, 3, 4, 5, 6}; + long acc = 0; + acc += add_shl(100, 5); /* 140 */ + acc += sub_shl(100, 5); /* 80 */ + acc += multi_use(100, 5); /* 140; g=40 */ + acc += g; /* +40 */ + acc += load_neg(la + 2, -2); /* la[0] = 7 (negative index) */ + acc += (long)load_u(ua, 3); /* 400 */ + acc += shared(la, 4); /* la[4]=13; g=4 */ + acc += g; /* +4 */ + acc += sum(ia, 6); /* 21 + 60 = 81 */ + /* total = 140+80+140+40+7+400+13+4+81 = 905 */ + return (int)(acc - 905); /* 0 on success */ +} +EOF +if [ -n "$SYS" ]; then + "$KIT" cc -O0 --sysroot "$SYS" "$WORK/run.c" -o "$WORK/run_o0" -lc \ + > "$WORK/run0.cc" 2>&1 || fail "rider O0 link failed" "$WORK/run0.cc" + "$KIT" cc -O1 --sysroot "$SYS" "$WORK/run.c" -o "$WORK/run_o1" -lc \ + > "$WORK/run1.cc" 2>&1 || fail "rider O1 link failed" "$WORK/run1.cc" + if [ "$do_struct" = 1 ]; then + "$WORK/run_o0"; rc0=$? + "$WORK/run_o1"; rc1=$? + [ "$rc0" -eq 0 ] || fail "rider O0 run wrong result rc=$rc0" + [ "$rc1" -eq 0 ] || fail "rider O1 run wrong result rc=$rc1" + [ "$rc0" -eq "$rc1" ] || fail "rider O0 ($rc0) != O1 ($rc1)" + printf ' rider: O0==O1 (rc=%s) — both folds correct incl. negative index\n' "$rc0" + else + printf ' rider: built O0/O1 OK; exec skipped (non-aa64 host)\n' + fi +else + printf ' rider: correctness skipped (no SDK)\n' +fi + +echo "o1p_rider: OK"