kit

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

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:
Mdoc/MIR_RA_REPORT.md | 45++++++++++++++++++++++++++-------------------
Msrc/opt/opt.c | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/opt/phase0_guardrails.sh | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/test.mk | 3++-
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 $@)