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:
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"