commit 86f37f3912b20a33fd054e34647e7e345b19233f
parent 1fc79efe18e7f925fad06ad0323ee4ea25edcc5f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 14 May 2026 18:42:19 -0700
RA phase 0 metrics and guardrails
Diffstat:
4 files changed, 231 insertions(+), 20 deletions(-)
diff --git a/doc/MIR_RA_REPORT.md b/doc/MIR_RA_REPORT.md
@@ -506,25 +506,32 @@ land with focused tests and metrics before moving to the next phase.
### Phase 0: Baseline and Guardrails
-- [ ] Preserve current O0 behavior as the reference path.
-- [ ] Keep existing O1 tests green before structural changes begin.
-- [ ] Add or identify stress inputs for:
- - [ ] one large straight-line function
- - [ ] many small functions
- - [ ] branch liveness
- - [ ] call-clobber preservation
- - [ ] spills
- - [ ] tied hard registers from inline asm
-- [ ] Keep `cfree run --time -O1` metrics available throughout the rewrite.
-- [ ] Add metrics names for the new shape before deleting old counters:
- - [ ] `opt.live.blocks`
- - [ ] `opt.live.active_words`
- - [ ] `opt.ranges`
- - [ ] `opt.range_points`
- - [ ] `opt.alloc.used_loc_words`
- - [ ] `opt.alloc.spills`
- - [ ] `opt.rewrite.reloads`
- - [ ] `opt.rewrite.stores`
+- [x] Preserve current O0 behavior as the reference path.
+- [x] Keep existing O1 tests green before structural changes begin.
+- [x] Add or identify stress inputs for:
+ - [x] one large straight-line function
+ - [x] many small functions
+ - [x] branch liveness
+ - [x] call-clobber preservation
+ - [x] spills
+ - [x] tied hard registers from inline asm
+- [x] Keep `cfree run --time -O1` metrics available throughout the rewrite.
+- [x] Add metrics names for the new shape before deleting old counters:
+ - [x] `opt.live.blocks`
+ - [x] `opt.live.active_words`
+ - [x] `opt.ranges`
+ - [x] `opt.range_points`
+ - [x] `opt.alloc.used_loc_words`
+ - [x] `opt.alloc.spills`
+ - [x] `opt.rewrite.reloads`
+ - [x] `opt.rewrite.stores`
+
+Implemented by `test/opt/phase0_guardrails.sh`, wired into `make test-opt`.
+The script runs generated O0/O1 guardrail programs for the stress shapes above
+and checks that `cfree run --time -O1` emits the reserved metric names. Inline
+asm tied-register stress is identified by the existing parser case
+`test/parse/cases/asm_01_grammar.c`; allocator-specific inline-asm execution
+coverage belongs in a later pass once the range allocator owns rewrite.
### Phase 1: Pass-Local Liveness
diff --git a/src/opt/opt.c b/src/opt/opt.c
@@ -1340,6 +1340,78 @@ static u64 func_inst_count(Func* f) {
return n;
}
+static u64 bitset_active_words(const u64* bits, u32 words) {
+ u64 n = 0;
+ if (!bits) return 0;
+ for (u32 i = 0; i < words; ++i)
+ if (bits[i]) ++n;
+ return n;
+}
+
+static u64 func_live_active_words(Func* f) {
+ u64 n = 0;
+ if (!f) return 0;
+ for (u32 b = 0; b < f->nblocks; ++b) {
+ Block* bl = &f->blocks[b];
+ n += bitset_active_words(bl->live_in, f->opt_live_words);
+ n += bitset_active_words(bl->live_out, f->opt_live_words);
+ n += bitset_active_words(bl->live_use, f->opt_live_words);
+ n += bitset_active_words(bl->live_def, f->opt_live_words);
+ }
+ return n;
+}
+
+static u64 func_live_value_count(Func* f) {
+ u64 n = 0;
+ if (!f || !f->val_info) return 0;
+ for (Val v = 1; v < f->nvals; ++v)
+ if (f->val_info[v].first_pos) ++n;
+ return n;
+}
+
+static int inst_spill_local(Func* f, const Inst* in, u32 op_idx) {
+ FrameSlot fs;
+ if (!f || !in || op_idx >= in->nopnds) return 0;
+ if (in->opnds[op_idx].kind != OPK_LOCAL) return 0;
+ fs = in->opnds[op_idx].v.frame_slot;
+ return fs != FRAME_SLOT_NONE && fs <= f->nframe_slots &&
+ f->frame_slots[fs - 1u].kind == FS_SPILL;
+}
+
+static u64 func_spill_alloc_count(Func* f) {
+ u64 n = 0;
+ if (!f || !f->val_info) return 0;
+ for (Val v = 1; v < f->nvals; ++v)
+ if (f->val_info[v].alloc_kind == OPT_ALLOC_SPILL) ++n;
+ return n;
+}
+
+static u64 func_spill_load_count(Func* f) {
+ u64 n = 0;
+ if (!f) return 0;
+ for (u32 b = 0; b < f->nblocks; ++b) {
+ Block* bl = &f->blocks[b];
+ for (u32 i = 0; i < bl->ninsts; ++i) {
+ Inst* in = &bl->insts[i];
+ if ((IROp)in->op == IR_LOAD && inst_spill_local(f, in, 1)) ++n;
+ }
+ }
+ return n;
+}
+
+static u64 func_spill_store_count(Func* f) {
+ u64 n = 0;
+ if (!f) return 0;
+ for (u32 b = 0; b < f->nblocks; ++b) {
+ Block* bl = &f->blocks[b];
+ for (u32 i = 0; i < bl->ninsts; ++i) {
+ Inst* in = &bl->insts[i];
+ if ((IROp)in->op == IR_STORE && inst_spill_local(f, in, 0)) ++n;
+ }
+ }
+ return n;
+}
+
/* ---- func_end: optionally run dry-run passes; replay; reset ---- */
static void w_func_end(CGTarget* t) {
@@ -1364,6 +1436,11 @@ static void w_func_end(CGTarget* t) {
metrics_scope_begin(o->c, "opt.live_info.pre_dde");
opt_live_info(o->f);
metrics_count(o->c, "opt.live_words", o->f->opt_live_words);
+ metrics_count(o->c, "opt.live.blocks", o->f->nblocks);
+ metrics_count(o->c, "opt.live.active_words",
+ func_live_active_words(o->f));
+ metrics_count(o->c, "opt.ranges", func_live_value_count(o->f));
+ metrics_count(o->c, "opt.range_points", o->f->opt_position_count);
metrics_count(o->c, "opt.conflict_bytes",
(u64)o->f->nvals * (u64)o->f->opt_conflict_words *
(u64)sizeof(u64));
@@ -1374,6 +1451,10 @@ static void w_func_end(CGTarget* t) {
o->f->val_info = NULL; /* force opt_regalloc to recompute liveness */
metrics_scope_begin(o->c, "opt.regalloc");
opt_regalloc(o->f, 0);
+ metrics_count(o->c, "opt.alloc.used_loc_words", 0);
+ metrics_count(o->c, "opt.alloc.spills", func_spill_alloc_count(o->f));
+ metrics_count(o->c, "opt.rewrite.reloads", func_spill_load_count(o->f));
+ metrics_count(o->c, "opt.rewrite.stores", func_spill_store_count(o->f));
metrics_scope_end(o->c, "opt.regalloc");
metrics_scope_begin(o->c, "opt.combine");
opt_combine(o->f);
diff --git a/test/opt/phase0_guardrails.sh b/test/opt/phase0_guardrails.sh
@@ -0,0 +1,122 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+BIN="${CFREE_BIN:-$ROOT/build/cfree}"
+TMP="${TMPDIR:-/tmp}/cfree-opt-phase0.$$"
+
+mkdir -p "$TMP"
+trap 'rm -rf "$TMP"' EXIT
+
+write_branch_liveness() {
+ cat >"$TMP/branch_liveness.c" <<'SRC'
+int main() {
+ int x = 7;
+ int y = 0;
+ if (x)
+ y = x + 3;
+ else
+ y = x + 5;
+ return y == 10 ? 0 : 1;
+}
+SRC
+}
+
+write_call_clobber() {
+ cat >"$TMP/call_clobber.c" <<'SRC'
+static int bump(int x) { return x + 1; }
+int main() {
+ int a = 3;
+ int b = 5;
+ int c = bump(a);
+ return (a == 3 && b == 5 && c == 4) ? 0 : 1;
+}
+SRC
+}
+
+write_spills() {
+ {
+ printf 'int main() {\n'
+ for i in $(seq 0 39); do
+ printf ' int a%d = %d;\n' "$i" "$i"
+ done
+ printf ' int s = 0;\n'
+ for i in $(seq 0 39); do
+ printf ' s += a%d;\n' "$i"
+ done
+ printf ' return s == 780 ? 0 : 1;\n'
+ printf '}\n'
+ } >"$TMP/spills.c"
+}
+
+write_many_small_functions() {
+ {
+ for i in $(seq 0 31); do
+ printf 'static int f%d(int x) { return x + %d; }\n' "$i" "$i"
+ done
+ printf 'int main() {\n int s = 0;\n'
+ for i in $(seq 0 31); do
+ printf ' s += f%d(1);\n' "$i"
+ done
+ printf ' return s == 528 ? 0 : 1;\n}\n'
+ } >"$TMP/many_small_functions.c"
+}
+
+write_large_straight_line() {
+ local expected=0
+ local x=3
+ {
+ printf 'int main() {\n int s = 0;\n'
+ for i in $(seq 0 127); do
+ expected=$((expected + i * ((x + i) & 7)))
+ printf ' s += %d * ((%d + %d) & 7);\n' "$i" "$x" "$i"
+ done
+ printf ' return s == %d ? 0 : 1;\n}\n' "$expected"
+ } >"$TMP/large_straight_line.c"
+}
+
+run_case() {
+ local name="$1"
+ local src="$2"
+ for opt in -O0 -O1; do
+ "$BIN" run "$opt" "$src" >/dev/null
+ done
+ printf 'phase0 %-24s O0/O1 OK\n' "$name"
+}
+
+check_metrics() {
+ local src="$TMP/branch_liveness.c"
+ local err="$TMP/metrics.err"
+ "$BIN" run --time -O1 "$src" >/dev/null 2>"$err"
+ for metric in \
+ opt.live.blocks \
+ opt.live.active_words \
+ opt.ranges \
+ opt.range_points \
+ opt.alloc.used_loc_words \
+ opt.alloc.spills \
+ opt.rewrite.reloads \
+ opt.rewrite.stores; do
+ if ! grep -q "$metric" "$err"; then
+ echo "phase0 metrics: missing $metric" >&2
+ sed -n '1,160p' "$err" >&2
+ exit 1
+ fi
+ done
+ printf 'phase0 metrics OK\n'
+}
+
+write_branch_liveness
+write_call_clobber
+write_spills
+write_many_small_functions
+write_large_straight_line
+
+run_case branch_liveness "$TMP/branch_liveness.c"
+run_case call_clobber "$TMP/call_clobber.c"
+run_case spills "$TMP/spills.c"
+run_case many_small_functions "$TMP/many_small_functions.c"
+run_case large_straight_line "$TMP/large_straight_line.c"
+check_metrics
+
+printf 'phase0 identified inline-asm stress: test/parse/cases/asm_01_grammar.c\n'
diff --git a/test/test.mk b/test/test.mk
@@ -169,8 +169,9 @@ test-link: lib $(ROUNDTRIP_BIN) $(ROUNDTRIP_BIN_MACHO) $(LINK_EXE_RUNNER) $(JIT_
OPT_TEST_BIN = build/test/opt_test
-test-opt: $(OPT_TEST_BIN)
+test-opt: bin $(OPT_TEST_BIN)
bash test/opt/run.sh
+ bash test/opt/phase0_guardrails.sh
$(OPT_TEST_BIN): test/opt/opt_test.c $(LIB_AR)
@mkdir -p $(dir $@)