commit 235a72c755e77fd64a69c25dc8f38201f9f10776
parent a8d7f4c28bb7c82d09f317d3517c305bb4bee9f9
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 01:58:15 -0700
opt: local same-block redundant-load + CSE elimination at O1 (O1.md W5)
Diffstat:
3 files changed, 482 insertions(+), 2 deletions(-)
diff --git a/mk/test.mk b/mk/test.mk
@@ -859,7 +859,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: 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
$(OPT_TEST_BIN)
@@ -937,6 +937,12 @@ test-opt-o1-cmp-imm: bin
test-opt-o1-stack-dse: bin
@KIT=$(abspath $(BIN)) bash test/opt/o1_stack_dse.sh
+# Behavioral+structural check: same-block redundant-load/expression CSE reuses
+# the last load/compute, but never across a may-alias store, call, or volatile.
+.PHONY: test-opt-o1-local-cse
+test-opt-o1-local-cse: bin
+ @KIT=$(abspath $(BIN)) bash test/opt/o1_local_cse.sh
+
test-opt-tiny-inline: bin $(TINY_INLINE_TEST_BIN)
$(TINY_INLINE_TEST_BIN)
diff --git a/src/opt/pass_combine.c b/src/opt/pass_combine.c
@@ -405,6 +405,49 @@ typedef struct AddrCseEntry {
Operand addr; /* the address operand (OPK_LOCAL / OPK_GLOBAL) */
} AddrCseEntry;
+/* W5 (O1.md): local (same-block) redundant-load + pure-compute CSE. Two small
+ * most-recent-wins rings:
+ *
+ * - LOAD ring: the most-recently-loaded `{address, size, addr_space}` and the
+ * register that received the value. A later plain (non-observable,
+ * non-bit-field) IR_LOAD of the IDENTICAL address+shape, with the producer
+ * register still live and NO intervening memory write, is rewritten to an
+ * IR_COPY off the producer. Aliasing is the whole game: ANY memory write
+ * (inst_writes_memory: store/aggregate/atomic/call/asm/intrinsic) forgets
+ * the entire load ring in O(1), so a may-aliasing store between the two
+ * loads always preserves the reload. Volatile/atomic loads
+ * (opt_mem_observable) are never recorded or reused.
+ *
+ * - COMPUTE ring: the most-recent pure IR_BINOP/IR_UNOP `{op, operands}` and
+ * its destination register. A later identical pure compute whose inputs are
+ * unchanged since (and whose producer is still live) is rewritten to an
+ * IR_COPY off the producer. Pure computes touch no memory, so only register
+ * redefinitions/clobbers invalidate them (tracked via ctx->last_def, which a
+ * clobber barrier bumps for every register).
+ *
+ * Both rings are bounded → O(1) per inst → the pass stays linear. Each rewrite
+ * is the broader, map-based same-block form distinct from the adjacent
+ * store/load pairs opt_combine_compact_block already coalesces. */
+enum { COMBINE_LOAD_CSE_SLOTS = 8 };
+enum { COMBINE_COMPUTE_CSE_SLOTS = 8 };
+
+typedef struct LoadCseEntry {
+ i32 inst_idx; /* producing IR_LOAD index in this BB, -1 if empty */
+ Operand dst; /* the register that received the loaded value (OPK_REG) */
+ Operand addr; /* the load's address operand (OPK_INDIRECT/LOCAL/GLOBAL) */
+ u32 size; /* MemAccess size */
+ u16 addr_space;
+} LoadCseEntry;
+
+typedef struct ComputeCseEntry {
+ i32 inst_idx; /* producing IR_BINOP/IR_UNOP index in this BB, -1 if empty */
+ Operand dst; /* the result register (OPK_REG) */
+ u16 op; /* IROp (IR_BINOP / IR_UNOP) */
+ i64 sub; /* extra.imm = BinOp/UnOp selector */
+ Operand a; /* operand 1 */
+ Operand b; /* operand 2 (kind == 0 sentinel for a unary compute) */
+} ComputeCseEntry;
+
typedef struct CombineCtx {
Func* f;
Block* bl;
@@ -417,6 +460,11 @@ typedef struct CombineCtx {
/* W1a addr-of CSE ring (most-recent-wins). */
AddrCseEntry addr_cse[COMBINE_ADDR_CSE_SLOTS];
u32 addr_cse_next;
+ /* W5 local same-block load + pure-compute CSE rings (most-recent-wins). */
+ LoadCseEntry load_cse[COMBINE_LOAD_CSE_SLOTS];
+ u32 load_cse_next;
+ ComputeCseEntry compute_cse[COMBINE_COMPUTE_CSE_SLOTS];
+ u32 compute_cse_next;
/* W6 cmp-immediate tracking: the constant currently held by each integer
* hard register, if known. const_valid is a per-RC_INT-reg bit set when a
* reaching IR_LOAD_IMM defined that register and nothing since has redefined
@@ -435,6 +483,12 @@ static void ctx_reset(CombineCtx* ctx) {
for (u32 k = 0; k < COMBINE_ADDR_CSE_SLOTS; ++k)
ctx->addr_cse[k].inst_idx = -1;
ctx->addr_cse_next = 0;
+ for (u32 k = 0; k < COMBINE_LOAD_CSE_SLOTS; ++k)
+ ctx->load_cse[k].inst_idx = -1;
+ ctx->load_cse_next = 0;
+ for (u32 k = 0; k < COMBINE_COMPUTE_CSE_SLOTS; ++k)
+ ctx->compute_cse[k].inst_idx = -1;
+ ctx->compute_cse_next = 0;
ctx->const_valid = 0;
ctx->block_change_p = 0;
}
@@ -1387,6 +1441,232 @@ static int try_addr_of_cse(CombineCtx* ctx, Inst* in, i32 i) {
return 0;
}
+/* ---- Rewrite 8 (W5): local same-block redundant-load elimination ---- */
+
+/* Two load-address operands name the same address iff identical kind+payload.
+ * For an OPK_INDIRECT both the base, index, scale, AND offset must match; the
+ * register-unchanged-since check is applied separately by the caller. */
+static int same_load_addr_operand(const Operand* a, const Operand* b) {
+ if (a->kind != b->kind) return 0;
+ switch (a->kind) {
+ case OPK_LOCAL:
+ return a->v.frame_slot == b->v.frame_slot;
+ case OPK_GLOBAL:
+ return a->v.global.sym == b->v.global.sym &&
+ a->v.global.addend == b->v.global.addend;
+ case OPK_INDIRECT:
+ 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.ofs == b->v.ind.ofs;
+ default:
+ return 0;
+ }
+}
+
+/* A plain, CSE-eligible IR_LOAD: dst is a register, address is a
+ * direct/indirect memory operand, and the access is neither observable
+ * (volatile/atomic) nor a bit-field rider. */
+static int load_is_cseable(const Inst* in) {
+ if ((IROp)in->op != IR_LOAD || in->nopnds < 2) return 0;
+ if (in->opnds[0].kind != OPK_REG) return 0;
+ const Operand* addr = &in->opnds[1];
+ /* W5 owns pointer-deref (OPK_INDIRECT) and global (OPK_GLOBAL) loads — the
+ * `*p` / global-variable same-block reload. Direct frame-slot loads
+ * (OPK_LOCAL spill/local reloads) are deliberately EXCLUDED: that traffic is
+ * the domain of the adjacent spill compaction in opt_combine_compact_block
+ * (store/load, load/load, load/store, store/store pairs) and the linear-scan
+ * spill machinery, which the doc says to keep distinct from W5. Reusing a
+ * spill reload here both duplicates that machinery and is unsound against the
+ * way the spill/coalesce path may re-home a frame slot, so it stays out. */
+ if (addr->kind != OPK_INDIRECT && addr->kind != OPK_GLOBAL) return 0;
+ if (opt_mem_observable(&in->extra.mem)) return 0;
+ if (in->extra.mem.bf_width != 0) return 0; /* bit-field rider */
+ /* Self-clobbering load (`ldr x9, [x9, #k]`): the destination overwrites its
+ * own address base/index, so after the load the address register no longer
+ * holds the loaded-from pointer. The recorded `addr` operand would then match
+ * a syntactically-identical later load whose base now denotes a DIFFERENT
+ * address — an unsound reuse. Exclude these from both recording and reuse:
+ * the same-operand check (load_addr_regs_unchanged, ctx_def_changed_since)
+ * uses a strictly-greater index test and cannot see a redefinition that
+ * happened AT the recorded load's own index. */
+ if (in->opnds[0].cls == RC_INT && addr->kind == OPK_INDIRECT) {
+ Reg d = in->opnds[0].v.reg;
+ if (addr->v.ind.base == d) return 0;
+ if (addr->v.ind.index != (Reg)REG_NONE && addr->v.ind.index == d) return 0;
+ }
+ return 1;
+}
+
+/* The address registers (base/index of an OPK_INDIRECT) must not have been
+ * redefined since the earlier load produced `dst`. For a direct OPK_LOCAL /
+ * OPK_GLOBAL there is no register input — the address is immutable. */
+static int load_addr_regs_unchanged(const CombineCtx* ctx, const Operand* addr,
+ i32 since_idx) {
+ if (addr->kind != OPK_INDIRECT) return 1;
+ if (addr->v.ind.base != (Reg)REG_NONE &&
+ ctx_def_changed_since(ctx, RC_INT, addr->v.ind.base, since_idx))
+ return 0;
+ if (addr->v.ind.index != (Reg)REG_NONE &&
+ ctx_def_changed_since(ctx, RC_INT, addr->v.ind.index, since_idx))
+ return 0;
+ return 1;
+}
+
+static void load_cse_record(CombineCtx* ctx, const Inst* in, i32 i) {
+ if (!load_is_cseable(in)) return;
+ LoadCseEntry* e = &ctx->load_cse[ctx->load_cse_next];
+ e->inst_idx = i;
+ e->dst = in->opnds[0];
+ e->addr = in->opnds[1];
+ e->size = in->extra.mem.size;
+ e->addr_space = in->extra.mem.addr_space;
+ ctx->load_cse_next = (ctx->load_cse_next + 1u) % COMBINE_LOAD_CSE_SLOTS;
+}
+
+static int try_local_load_cse(CombineCtx* ctx, Inst* in, i32 i) {
+ if (!load_is_cseable(in)) return 0;
+ const Operand* addr = &in->opnds[1];
+ u32 size = in->extra.mem.size;
+ u16 as = in->extra.mem.addr_space;
+ for (u32 k = 0; k < COMBINE_LOAD_CSE_SLOTS; ++k) {
+ const LoadCseEntry* e = &ctx->load_cse[k];
+ if (e->inst_idx < 0 || e->inst_idx >= i) continue;
+ if (e->size != size || e->addr_space != as) continue;
+ if (!same_load_addr_operand(&e->addr, addr)) continue;
+ /* ALIASING: any memory write between the earlier load and now invalidates
+ * reuse. ctx->last_mem_def is the index of the most-recent memory writer
+ * (set by ctx_record for every inst_writes_memory inst, including the
+ * call/asm/intrinsic clobber barrier). If it occurred after the earlier
+ * load, the loaded value may have changed -> do not reuse. */
+ if (ctx->last_mem_def > e->inst_idx) continue;
+ /* The producer register must still hold the loaded value: it is this BB's
+ * live producer of (cls,reg) and was not redefined/clobbered since. */
+ if (ctx_producer_of(ctx, e->dst.cls, e->dst.v.reg) != e->inst_idx) continue;
+ if (ctx_def_changed_since(ctx, e->dst.cls, e->dst.v.reg, e->inst_idx))
+ continue;
+ /* The address-forming registers must be unchanged since the earlier load,
+ * else the two indirects compute different addresses. */
+ if (!load_addr_regs_unchanged(ctx, addr, e->inst_idx)) continue;
+ /* A copy of the producer reg into self is a no-op the dst already holds. */
+ if (same_phys_reg(&in->opnds[0], &e->dst)) continue;
+ /* Same-class reuse only (an int load into an FP reg, or vice versa, would
+ * need a cross-class move, not a same-class copy). */
+ if (in->opnds[0].cls != e->dst.cls) continue;
+ /* Never reuse a value that lives in a target scratch register: native emit
+ * repurposes the scratch regs to materialize OPK_LOCAL operands between MIR
+ * instructions (invisible to MIR-level liveness), so a new copy off such a
+ * register can read a clobbered value. */
+ if (is_target_scratch_reg(ctx->f, e->dst.cls, e->dst.v.reg)) continue;
+ /* Rewrite `load rD, [addr]` into `copy rD, rP`. */
+ in->op = (u16)IR_COPY;
+ in->opnds[1] = e->dst;
+ in->nopnds = 2;
+ ctx->block_change_p = 1;
+ return 1;
+ }
+ return 0;
+}
+
+/* ---- Rewrite 9 (W5): local same-block pure-compute CSE ---- */
+
+/* A pure compute eligible for same-block CSE: IR_BINOP or IR_UNOP with a
+ * register destination. (IR_CMP is excluded — its result feeds branch/flag
+ * lowering and is not a freely-copyable register value here.) */
+static int compute_is_cseable(const Inst* in) {
+ IROp op = (IROp)in->op;
+ if (op != IR_BINOP && op != IR_UNOP) return 0;
+ if (in->nopnds < 1 || in->opnds[0].kind != OPK_REG) return 0;
+ if (op == IR_BINOP && in->nopnds < 3) return 0;
+ if (op == IR_UNOP && in->nopnds < 2) return 0;
+ return 1;
+}
+
+/* Two compute operands match iff identical kind+payload. Only REG and IMM
+ * operands appear as binop/unop inputs at this stage; any other kind is treated
+ * as non-matching (conservative). */
+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;
+ case OPK_IMM:
+ return a->v.imm == b->v.imm && a->type == b->type;
+ default:
+ return 0;
+ }
+}
+
+/* A compute input register must be unchanged since the earlier compute produced
+ * its result, else the recomputation would read different values. */
+static int compute_input_unchanged(const CombineCtx* ctx, const Operand* op,
+ i32 since_idx) {
+ if (op->kind != OPK_REG) return 1; /* immediates never change */
+ return !ctx_def_changed_since(ctx, op->cls, op->v.reg, since_idx);
+}
+
+static void compute_cse_record(CombineCtx* ctx, const Inst* in, i32 i) {
+ if (!compute_is_cseable(in)) return;
+ ComputeCseEntry* e = &ctx->compute_cse[ctx->compute_cse_next];
+ e->inst_idx = i;
+ e->dst = in->opnds[0];
+ e->op = in->op;
+ e->sub = in->extra.imm;
+ e->a = in->opnds[1];
+ if ((IROp)in->op == IR_BINOP)
+ e->b = in->opnds[2];
+ else
+ memset(&e->b, 0, sizeof e->b); /* unary sentinel: kind == 0 */
+ ctx->compute_cse_next =
+ (ctx->compute_cse_next + 1u) % COMBINE_COMPUTE_CSE_SLOTS;
+}
+
+static int try_local_compute_cse(CombineCtx* ctx, Inst* in, i32 i) {
+ if (!compute_is_cseable(in)) return 0;
+ IROp op = (IROp)in->op;
+ const Operand* a = &in->opnds[1];
+ const Operand* b = (op == IR_BINOP) ? &in->opnds[2] : NULL;
+ for (u32 k = 0; k < COMBINE_COMPUTE_CSE_SLOTS; ++k) {
+ const ComputeCseEntry* e = &ctx->compute_cse[k];
+ if (e->inst_idx < 0 || e->inst_idx >= i) continue;
+ if (e->op != in->op || e->sub != in->extra.imm) continue;
+ if (!same_compute_operand(&e->a, a)) continue;
+ if (op == IR_BINOP) {
+ if (!same_compute_operand(&e->b, b)) continue;
+ } else if (e->b.kind != 0) {
+ continue; /* recorded as binary; this is unary -> mismatch */
+ }
+ /* The producer's result register must still be its live producer and
+ * unchanged since (so the copy reads the same value). */
+ if (ctx_producer_of(ctx, e->dst.cls, e->dst.v.reg) != e->inst_idx) continue;
+ if (ctx_def_changed_since(ctx, e->dst.cls, e->dst.v.reg, e->inst_idx))
+ continue;
+ /* Inputs must be unchanged since the earlier compute, else its recorded
+ * result no longer equals recomputing here. */
+ if (!compute_input_unchanged(ctx, a, e->inst_idx)) continue;
+ if (op == IR_BINOP && !compute_input_unchanged(ctx, b, e->inst_idx))
+ continue;
+ /* Self-copy is a no-op; skip. Same class only. */
+ if (same_phys_reg(&in->opnds[0], &e->dst)) continue;
+ if (in->opnds[0].cls != e->dst.cls) continue;
+ /* Never reuse a value that lives in a target scratch register: the
+ * post-combine native emit repurposes the scratch regs to materialize
+ * OPK_LOCAL operands BETWEEN MIR instructions (invisible to MIR-level
+ * liveness), so extending such a register's live range to a new copy reads
+ * a clobbered value. (Same hazard the copy-prop path guards in
+ * try_substitute_for_reg.) */
+ if (is_target_scratch_reg(ctx->f, e->dst.cls, e->dst.v.reg)) continue;
+ /* Rewrite the recompute into `copy rD, rP`. */
+ in->op = (u16)IR_COPY;
+ in->opnds[1] = e->dst;
+ in->nopnds = 2;
+ in->extra.imm = 0;
+ ctx->block_change_p = 1;
+ return 1;
+ }
+ return 0;
+}
+
/* ---- Existing IR_RET retarget (kept; runs in the forward pass) ---- */
static int try_ret_retarget(Func* f, Block* bl, i32 i) {
@@ -1604,14 +1884,25 @@ static int opt_combine_fold_block(Func* f, Block* bl,
try_combine_exts(&ctx, in, i);
try_substitute(&ctx, in, i);
try_addr_synth(&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
+ * inst into an IR_COPY off the still-live earlier producer; copy-prop +
+ * DCE then retire it (worst case: a same-cost register move). */
+ try_local_load_cse(&ctx, in, i);
+ try_local_compute_cse(&ctx, in, i);
/* W6: fold a register holding a known constant into a compare's inline
* immediate slot (reads the constant tracker maintained below). */
try_cmp_imm_fold(&ctx, in);
}
/* Track this inst as an addr-of producer for later W1a CSE (only when it is
- * still an addr-of: a prior rewrite may have turned it into a copy). */
+ * still an addr-of: a prior rewrite may have turned it into a copy). Same
+ * for the W5 load / pure-compute rings (recorded only when the inst is
+ * still a load / binop / unop after the rewrites above). */
addr_cse_record(&ctx, in, i);
+ load_cse_record(&ctx, in, i);
+ compute_cse_record(&ctx, in, i);
ctx_record(&ctx, in, i);
/* W6: update the per-register known-constant tracker for this inst (after
* rewrites, so a load_imm produced/rewritten here is recorded). */
diff --git a/test/opt/o1_local_cse.sh b/test/opt/o1_local_cse.sh
@@ -0,0 +1,183 @@
+#!/usr/bin/env bash
+# Structural + correctness checks for the -O1 W5 same-block redundant-load +
+# CSE elimination (O1.md W5), implemented in src/opt/pass_combine.c
+# (try_local_load_cse / try_local_compute_cse).
+#
+# W5 is a single forward pass per block that reuses the last load of an
+# address (and the last result of a pure expression) until a store / call /
+# memory barrier invalidates it. It is deliberately conservative: ALIASING is
+# the whole game -- a load is reused only when NO intervening store may alias
+# (and a write to memory is treated as a full barrier that forgets every
+# pending load), and a pure compute is reused only when its inputs are
+# unchanged since.
+#
+# This guard validates:
+# A. ELIMINATION: a same-block redundant load of the same address with no
+# intervening aliasing store between is eliminated (the second ldr is
+# reused from a register, not re-loaded).
+# B. ALIAS-HAZARD PRESERVATION: a load, then a store that MAY alias, then a
+# reload KEEPS the reload -- W5 must NOT delete a load that may see the
+# new value.
+# C. BARRIER PRESERVATION: a load across a call is not reused (the call is a
+# full barrier), and a volatile load is never reused.
+# D. RUNTIME CORRECTNESS: a function exercising same-block reload + recompute,
+# alias hazards, volatile, and a call barrier computes the identical,
+# correct result at -O0 and -O1.
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+KIT="${KIT:-$ROOT/build/kit}"
+WORK="$ROOT/build/test/opt/o1_local_cse"
+mkdir -p "$WORK"
+
+fail() {
+ printf 'o1_local_cse FAILED: %s\n' "$1" >&2
+ shift || true
+ for f in "$@"; do
+ printf ' --- %s ---\n' "$f" >&2
+ sed 's/^/ | /' "$f" >&2
+ done
+ exit 1
+}
+
+fn_body() { # $1 = file, $2 = symbol name -> 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"
+}
+
+# ---- A. elimination: redundant same-address load reused from a register ----
+# Two reads of the SAME global through the same pointer with no store between.
+# Both reads are of the SAME address with nothing in between that writes
+# memory, so W5 reuses the first load. We count the loads of the slot.
+SRC_A="$WORK/elim.c"
+cat > "$SRC_A" <<'EOF'
+/* Two dependent uses of *p with no intervening write: the second load of *p is
+ * redundant and W5 reuses the first. The address (p in x0) is unchanged and
+ * no store/call happens between the two loads. */
+long load_twice(long *p) {
+ long a = *p; /* ldr */
+ long b = *p + 1; /* redundant ldr -> reuse */
+ return a * b;
+}
+EOF
+OBJ_A="$WORK/elim.o"
+"$KIT" cc -target aarch64-linux-gnu -O1 -std=c11 -c "$SRC_A" \
+ -o "$OBJ_A" > "$WORK/elim.cc.out" 2>&1 || fail "elim compile failed" "$WORK/elim.cc.out"
+"$KIT" objdump -d "$OBJ_A" > "$WORK/elim.dis" 2>&1
+A="$(fn_body "$WORK/elim.dis" load_twice)"
+[ -n "$A" ] || fail "load_twice not found" "$WORK/elim.dis"
+# Exactly one ldr from the pointer should remain (the redundant second is reused).
+NLDR="$(printf '%s\n' "$A" | grep -cE '\bldr\b' || true)"
+[ "$NLDR" -le 1 ] || fail "expected <=1 ldr in load_twice (redundant load not reused, got $NLDR)" "$WORK/elim.dis"
+
+# ---- B. alias-hazard: store between two loads MUST keep the reload ----
+SRC_B="$WORK/alias.c"
+cat > "$SRC_B" <<'EOF'
+/* *p is read, then *q is written (q may alias p), then *p is read again. The
+ * reload of *p must NOT be eliminated: it may observe the value stored to *q. */
+long load_store_load(long *p, long *q, long v) {
+ long a = *p;
+ *q = v; /* may alias *p -- full barrier */
+ long b = *p; /* must reload */
+ return a + b;
+}
+EOF
+OBJ_B="$WORK/alias.o"
+"$KIT" cc -target aarch64-linux-gnu -O1 -std=c11 -c "$SRC_B" \
+ -o "$OBJ_B" > "$WORK/alias.cc.out" 2>&1 || fail "alias compile failed" "$WORK/alias.cc.out"
+"$KIT" objdump -d "$OBJ_B" > "$WORK/alias.dis" 2>&1
+B="$(fn_body "$WORK/alias.dis" load_store_load)"
+[ -n "$B" ] || fail "load_store_load not found" "$WORK/alias.dis"
+# Both loads of *p must survive (>=2 ldr): the store is a barrier.
+NLDR_B="$(printf '%s\n' "$B" | grep -cE '\bldr\b' || true)"
+[ "$NLDR_B" -ge 2 ] || fail "reload after may-alias store was wrongly eliminated (ldr=$NLDR_B)" "$WORK/alias.dis"
+# And a store must be present.
+printf '%s\n' "$B" | grep -qE '\bstr\b' || fail "store missing in load_store_load" "$WORK/alias.dis"
+
+# ---- C. barrier: load across a call + volatile load never reused ----
+SRC_C="$WORK/barrier.c"
+cat > "$SRC_C" <<'EOF'
+extern void sink(void);
+/* Load across a call: the call is a full memory barrier; the reload survives. */
+long load_call_load(long *p) {
+ long a = *p;
+ sink();
+ long b = *p;
+ return a + b;
+}
+/* Volatile load: never reused, each read must re-load. */
+long vol_twice(volatile long *p) {
+ long a = *p;
+ long b = *p;
+ return a + b;
+}
+EOF
+OBJ_C="$WORK/barrier.o"
+"$KIT" cc -target aarch64-linux-gnu -O1 -std=c11 -c "$SRC_C" \
+ -o "$OBJ_C" > "$WORK/barrier.cc.out" 2>&1 || fail "barrier compile failed" "$WORK/barrier.cc.out"
+"$KIT" objdump -d "$OBJ_C" > "$WORK/barrier.dis" 2>&1
+C1="$(fn_body "$WORK/barrier.dis" load_call_load)"
+[ -n "$C1" ] || fail "load_call_load not found" "$WORK/barrier.dis"
+NLDR_C1="$(printf '%s\n' "$C1" | grep -cE '\bldr\b' || true)"
+[ "$NLDR_C1" -ge 2 ] || fail "reload across a call was wrongly eliminated (ldr=$NLDR_C1)" "$WORK/barrier.dis"
+C2="$(fn_body "$WORK/barrier.dis" vol_twice)"
+[ -n "$C2" ] || fail "vol_twice not found" "$WORK/barrier.dis"
+NLDR_C2="$(printf '%s\n' "$C2" | grep -cE '\bldr\b' || true)"
+[ "$NLDR_C2" -ge 2 ] || fail "volatile load was wrongly reused (ldr=$NLDR_C2)" "$WORK/barrier.dis"
+
+# ---- D. runtime correctness at -O0 vs -O1 (native arch) ----
+SRC_D="$WORK/run.c"
+cat > "$SRC_D" <<'EOF'
+/* Freestanding (no libc): the result is reduced to a small exit code, so -O0
+ * and -O1 must produce identical exit status. Exercises same-block redundant
+ * loads + recompute + an alias hazard + a call barrier + a volatile read. */
+static long acc_calls = 0;
+long ext_call(long x) { acc_calls += x; return x * 2; }
+
+static long kern(long *p, long *q, volatile long *vp, long n) {
+ long s = 0;
+ for (long i = 0; i < n; ++i) {
+ long a = p[0]; /* load */
+ long b = p[0] + 7; /* redundant load -> reuse */
+ long c = (a * 3) + (a * 3); /* a*3 computed twice -> reuse one */
+ q[0] = i; /* may alias p[0]: later p[0] read must reload */
+ long d = p[0]; /* reload after the store (alias hazard) */
+ long w = vp[0] + vp[0]; /* volatile: never reused, two real reads */
+ s += a + b + c + d + w;
+ s += ext_call(i & 3); /* call barrier */
+ long e = p[1] + p[1]; /* p[1] redundant load -> reuse, after the call */
+ s += e;
+ }
+ return s + acc_calls;
+}
+
+int main(void) {
+ long arr[2] = {5, 9};
+ long other = 0;
+ volatile long vol = 2;
+ long r = 0;
+ for (long t = 0; t < 11; ++t) {
+ arr[0] = 3 + t; arr[1] = 100 - t; other = 0; acc_calls = 0; vol = t + 1;
+ r ^= kern(arr, &other, &vol, 17);
+ r ^= other; /* observe the aliasing store target */
+ }
+ return (int)(r & 0x7f);
+}
+EOF
+BIN_O0="$WORK/run_o0"
+BIN_O1="$WORK/run_o1"
+"$KIT" cc -O0 -std=c11 "$SRC_D" -o "$BIN_O0" > "$WORK/run.o0.cc.out" 2>&1 \
+ || fail "run -O0 compile failed" "$WORK/run.o0.cc.out"
+"$KIT" cc -O1 -std=c11 "$SRC_D" -o "$BIN_O1" > "$WORK/run.o1.cc.out" 2>&1 \
+ || fail "run -O1 compile failed" "$WORK/run.o1.cc.out"
+set +e
+"$BIN_O0"; EC_O0=$?
+"$BIN_O1"; EC_O1=$?
+set -e
+[ "$EC_O0" = "$EC_O1" ] || fail "O0/O1 exit codes differ: $EC_O0 vs $EC_O1"
+
+printf 'o1_local_cse: OK (elim=%s ldr; alias kept %s ldr; call kept %s; vol kept %s; run rc=%s)\n' \
+ "$NLDR" "$NLDR_B" "$NLDR_C1" "$NLDR_C2" "$EC_O0"