Files
AILang/bench/run.sh
T
Brummel 14a91f0ae5 iter boehm-retirement.1 (DONE 10/10): retire the transitional Boehm GC backend
Closes Gitea #4. Removes the Boehm-Demers-Weiser conservative GC
backend wholesale across six layers in one atomic iteration. After
this iter, `AllocStrategy` has two variants (`Rc`, `Bump`),
`--alloc=gc` is rejected at CLI parse with `unknown --alloc value`,
the libgc link arm is gone, and the design ledger describes RC
(canonical) + bump (raw-alloc bench-floor) as the only allocators.

Layer-by-layer summary:

  CLI surface — `crates/ail/src/main.rs`:
    `parse_alloc_strategy` arm `"gc" => Ok(AllocStrategy::Gc)`
    removed; error wording updated to `(expected `rc` or `bump`)`;
    clap-derive `value_parser = ["gc","bump","rc"]` allowlist on
    BOTH `Build` and `Run` subcommands DROPPED so that
    `parse_alloc_strategy` remains the sole gatekeeper for the
    unknown-value diagnostic (otherwise clap shadows the runtime
    diagnostic with `invalid value 'gc' for '--alloc'`, which would
    miss the milestone-pin's stderr substring check). The
    `default_value = "rc"` stays.

  Codegen — `crates/ailang-codegen/src/lib.rs`:
    `AllocStrategy::Gc` variant + `Default` derive removed (no
    caller of `AllocStrategy::default()` existed in the workspace,
    so the trait derivation was dead). `fn_name` (spec called it
    `runtime_alloc_fn` loosely; actual identifier is `fn_name`)
    drops the `Gc => "GC_malloc"` arm. `lower_workspace` and
    `lower_workspace_staticlib` defaults flip from `Gc` to `Rc`.
    In-source negative-complement codegen test (mod tests, lib.rs:3571ff)
    retargets from `AllocStrategy::Gc` to `AllocStrategy::Bump`
    (bump also doesn't emit per-type drop fns; the test's semantic
    "no drop fns under non-RC" is preserved).

  Link branch — `crates/ail/src/main.rs:2389ff`:
    The `match strategy { AllocStrategy::Gc => { ... cmd.arg("-lgc"); ... } }`
    arm and its libgc-link block are entirely gone. The surviving
    match exhausts on `Bump` and `Rc` (Rust's exhaustiveness check
    confirms; no `error[E0004]`). Staticlib-guard diagnostic
    rewritten to drop the "shared Boehm collector" phrasing while
    preserving the prefix `staticlib (swarm) artefact is RC-only`
    verbatim (the surviving `staticlib_bump_is_rejected` test
    depends on that substring).

  Test suite — 3 pure-differential e2e tests deleted
    (`gc_handles_recursive_list_construction`,
    `alloc_rc_produces_same_stdout_as_gc`,
    `alloc_rc_matches_gc_on_std_list_demo`); 9 RC-feature tests
    stripped of their `stdout_gc` build call and differential
    `assert_eq!(stdout_gc, stdout_rc, ...)` (absolute
    `assert_eq!(stdout_rc.trim(), "<n>")` pin retained as
    correctness oracle); `staticlib_gc_is_rejected` deleted; new
    milestone-pin `crates/ail/tests/boehm_retirement_pin.rs`
    asserts `ail build --alloc=gc` exits ≠ 0 with stderr containing
    `unknown --alloc value` and `\`gc\``; `examples/gc_stress.ail`
    fixture deleted (no remaining references).

    Implementer expansion (not in plan): `iter17a_local_box_alloca`
    (in `e2e.rs`) carried an IR-shape assertion against
    `@GC_malloc`-absence as the witness for non-escaping
    allocation. After the Task-2 codegen default flip, the witness
    shifts to `@ailang_rc_alloc`-absence in escape-targeted
    positions; assertion + doc-comment updated. Property
    protected ("no heap allocation in non-escaping contexts") is
    unchanged; only the named allocator shifts.

  Bench harness — `bench/run.sh` 9→6 column compaction
    (workload + bump(s) + rc(s) + rc/bump + bump RSS + rc RSS);
    gc-arm `bench_latency_implicit_gc` build call + harness
    invocation dropped from latency block; header comment reframed
    from "GC-overhead bench harness" to "RC-overhead bench
    harness"; "Decision 10's Boehm-retirement target (1.3x)"
    rewording to "RC-overhead-vs-bump bench-health regression gate".

    `bench/check.py:62` header-sentinel changes from
    `"gc(s)" in line` to `"bump(s)" in line`; column-count check
    at `:72` flips from `!= 9` to `!= 6`; per-workload field set
    drops `gc_s`/`gc_over_bump`/`gc_rss_kb`; `ARM_LABEL_TO_KEY`
    drops the `"implicit @ gc": "implicit_at_gc"` entry.
    `bench/baseline.json` regenerated via `--update-baseline`.

    Implementer note (planner-defect): `write_new_baseline`
    iterated over the *existing* baseline's metric list when
    emitting the regenerated file, so even after parser-level
    `gc_*` removal, the fallback emitted them back into the JSON.
    Scrubbed post-update; the cleaner fix (have
    `write_new_baseline` emit only keys present in
    `parsed_throughput[workload]`) is a follow-up if the script
    becomes load-bearing for further allocator changes.

  Design ledger — `design/models/rc-uniqueness.md` excises the
    `## Dual allocator — RC canonical, Boehm parity oracle`
    section and the `Boehm-Demers-Weiser conservative GC` choice
    block + rationale + trade-offs; the per-fn-alloca section
    generalises Boehm-specific language to allocator-agnostic;
    the memory-model section's `## Choice.` paragraph reframes the
    1.3× target from "Boehm-retirement gate" to "bench-health
    regression gate".

    `design/models/pipeline.md` drops the `--alloc=gc → links libgc`
    arm of the pipeline diagram and replaces it with
    `--alloc=bump → links bump-floor`; the accompanying prose
    rewrites accordingly.

    `design/contracts/scope-boundaries.md` rewrites the
    "Memory management via Boehm conservative GC" bullet to
    describe RC + per-fn-arena present-tense; the dead reference
    to `examples/gc_stress.ail.json` (file never existed; the
    fixture only ever had a `.ail` form, deleted by this iter) is
    dropped along with the `examples/std_list_stress.ail.json`
    reference whose purpose was Boehm-only soak testing.
    `:67`'s `@printf` / `@GC_malloc` parenthetical updated.

    `design/contracts/memory-model.md:232` drops the
    "leaks like the pre-Boehm era" phrase; the RC inc/dec
    instrumentation is wired up, so the "until then" conditional
    that referenced pre-Boehm is closed.

    `design/contracts/embedding-abi.md:42-44` rewrites the
    staticlib-guard prose to drop the `--alloc=gc` clause (gc is
    now a CLI-parser-level unknown-value, not a staticlib-guard
    rejection) and reframe the swarm-safety justification around
    `--alloc=bump` (leak-only bench instrument) rather than the
    historical Boehm collector.

  Honesty pin — `crates/ailang-core/tests/docs_honesty_pin.rs`
    inverts the polarity: the present-tense Boehm-anchor assertion
    on `pipeline.md` (`:116-117`) is deleted, and four
    absence-pins are added to `design_md_has_no_wunschdenken`
    against the Boehm-zombie strings `transitional Boehm`,
    `parity oracle`, `GC_malloc`, `libgc`. The
    `design_corpus()` already includes `rc-uniqueness.md` so no
    path-list change was needed for the new pins to scan.

    `crates/ailang-core/tests/design_index_pin.rs:166` drops the
    `"pre-Boehm"` token from the protected-exception comment list
    (the phrase no longer appears in `memory-model.md` after this
    iter, so the exception is dead).

  Runtime docs — `runtime/bump.c`, `runtime/rc.c`, `runtime/str.c`
    header comments scrubbed of Boehm/`GC_malloc`/`libgc`
    references. `bump.c`'s function signature description still
    documents `void *bump_malloc(size_t)` as the bench-floor
    allocator interface, but no longer cross-references libgc.

  Example fixtures — `examples/bench_latency_implicit.ail`,
    `bench_latency_explicit.ail`, `escape_local_demo.ail`,
    `reuse_as_demo.ail`, `rc_pin_recurse_implicit.ail` doc-comment
    headers scrubbed of `--alloc=gc` / Boehm references. The
    `.ail` surface (AST) is untouched in every case; round-trip
    invariant holds (`cargo test -p ailang-surface --test round_trip`
    green).

  Skill / agent prompts — `skills/audit/agents/ailang-bencher.md`
    rewritten to use an RC-vs-bump worked example pattern for the
    hypothesis-driven bench tutorial, replacing the recurring
    "RC vs Boehm under heap pressure" example.
    `skills/implement/agents/ailang-implementer.md` Decision-10 /
    Boehm references replaced with present-tense RC-commitment
    framing.

  IR snapshots — the 5 checked-in snapshots
    (`crates/ail/tests/snapshots/{hello,list,max3,sum,ws_main}.ll`)
    regenerated via `UPDATE_SNAPSHOTS=1 cargo test -p ail --test
    ir_snapshot`. Each previously contained
    `declare ptr @GC_malloc(i64)` and (for `list.ll`) a `call ptr
    @GC_malloc(...)` invocation; post-flip the snapshots contain
    `declare ptr @ailang_rc_alloc(i64)` plus the rc inc/dec runtime
    declarations.

Spec-vs-acceptance addendum (caught at orchestrator end-report,
absorbed here rather than in a follow-up spec edit): spec §6
acceptance criteria said "Boehm-grep returns matches ONLY in
docs_honesty_pin.rs". The plan itself prescribed historical Boehm
references in 3 additional files: (a) the new milestone-pin
`boehm_retirement_pin.rs` (must literally invoke `--alloc=gc` to
assert its rejection), (b) `embed_staticlib_alloc_guard.rs` file
doc-comment historical note ("`--alloc=gc` no longer exists as a
CLI value"), (c) `embedding-abi.md:44-45` contract historical
clause ("see the Boehm-retirement iter"). All three are
prescribed; the spec's grep wording was too narrow. The four
absence-pins in `docs_honesty_pin.rs` catch the actual zombies
(Boehm-narrative re-emerging in the design ledger), which is the
substantive intent the spec was aiming at — the four extra
documented-by-design exceptions are the cost of having an
explicit milestone-pin and contract-level historical anchors.

Net delta:
  - 32 files modified, 2 new (boehm_retirement_pin.rs + stats),
    1 deleted (gc_stress.ail);
  - workspace tests: every binary `0 failed`. Pass-count delta:
    -3 net (4 e2e tests deleted, 1 new milestone-pin test added);
  - boehm-grep state: hits only in the four by-design exceptions
    documented above;
  - `bench/check.py` exit 0 against regenerated baseline;
  - CLI must-fail fixture: `ail build --alloc=gc examples/hello.ail`
    exits non-zero with stderr containing `unknown --alloc value`
    and `\`gc\``;
  - design ledger present-tense honest (Boehm-narrative gone from
    `rc-uniqueness.md` + `pipeline.md`; the few historical
    references in `embedding-abi.md` / `boehm_retirement_pin.rs` /
    `embed_staticlib_alloc_guard.rs` are explicit milestone-pins
    or contract anchors, not silent ledger residue).

Bench measurement variance noted: closure-chain and hof-pipeline
are ±1-5% jittery between runs; one regeneration flagged 2
metrics as `regressed` before a second run returned 0. The
captured baseline is within self-comparison range. Existing
per-metric tolerances absorb the jitter.

Stats file:
`bench/orchestrator-stats/2026-05-20-iter-boehm-retirement.1.json`.

closes #4
2026-05-20 20:51:53 +02:00

206 lines
7.6 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#
# RC-overhead bench harness.
#
# Builds each fixture twice — `--alloc=rc` (canonical RC runtime) and
# `--alloc=bump` (no-free 256 MB arena from `runtime/bump.c`, the
# raw-alloc bench-floor). Runs each binary N times, drops the slowest
# run, takes the median wall time. The rc-over-bump ratio is the
# bench-health regression gate.
#
# Output: a table with bump-median, rc-median, rc/bump ratio, and max
# RSS for both modes. Designed to be captured verbatim into a commit
# body.
#
# Requirements: bash, /usr/bin/time -v (GNU coreutils), bc, sort, awk,
# a release-mode `ail` binary.
#
# Usage: bench/run.sh [-n RUNS]
# -n RUNS number of timed runs per binary (default 5; min 3 so we
# can drop the slowest and still take a median over 4).
set -euo pipefail
RUNS=5
while getopts "n:" opt; do
case $opt in
n) RUNS="$OPTARG" ;;
*) echo "usage: $0 [-n RUNS]" >&2; exit 2 ;;
esac
done
if (( RUNS < 3 )); then
echo "RUNS must be >= 3" >&2
exit 2
fi
# Anchor at the workspace root regardless of CWD.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT"
# We measure wall-clock + max RSS via a small Python helper that wraps
# the binary, calls `time.monotonic()` around `os.waitpid`, and reads
# `getrusage(RUSAGE_CHILDREN).ru_maxrss` (KB on Linux). This avoids a
# /usr/bin/time dependency (Arch / minimal containers often don't have
# the GNU coreutils `time` binary installed) without sacrificing
# either signal: monotonic clocks for wall time, kernel-reported peak
# resident set for RSS.
PY="$(command -v python3 || true)"
if [[ -z "$PY" ]]; then
echo "error: python3 is required for the timing helper" >&2
exit 2
fi
# Build the release `ail` binary if needed.
echo ">>> ensuring release ail binary"
cargo build --release -p ail >/dev/null
AIL="$ROOT/target/release/ail"
[[ -x "$AIL" ]] || { echo "ail binary missing: $AIL" >&2; exit 1; }
OUTDIR="$ROOT/target/bench"
mkdir -p "$OUTDIR"
# Compile both modes for both fixtures up front so the bench loop only
# measures runtime, not build time.
fixtures=(bench_list_sum bench_tree_walk bench_closure_chain bench_hof_pipeline bench_compute_collatz bench_list_sum_explicit)
modes=(bump rc)
echo ">>> compiling fixtures (-O2)"
for f in "${fixtures[@]}"; do
src="$ROOT/examples/$f.ail"
[[ -f "$src" ]] || { echo "missing fixture: $src" >&2; exit 1; }
for m in "${modes[@]}"; do
bin="$OUTDIR/${f}_${m}"
echo " $f --alloc=$m -> $bin"
"$AIL" build --opt=-O2 --alloc="$m" "$src" -o "$bin" >/dev/null
done
done
# Time one binary one time. Wraps the binary in a Python helper that
# measures wall-clock via time.monotonic() and max RSS (KB) via
# getrusage(RUSAGE_CHILDREN).ru_maxrss after the child exits. Stdout
# of the binary is discarded; we already verified correctness via a
# smoke run earlier. Output: "wall_seconds rss_kb" on a single line.
time_one() {
local bin="$1"
"$PY" -c '
import os, resource, subprocess, sys, time
bin_path = sys.argv[1]
t0 = time.monotonic()
p = subprocess.Popen([bin_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
p.wait()
t1 = time.monotonic()
ru = resource.getrusage(resource.RUSAGE_CHILDREN)
# ru_maxrss is in KB on Linux. We want the peak across only this child;
# RUSAGE_CHILDREN is cumulative across all children of the helper, but
# the helper only spawns this one child per invocation, so the value is
# this run.
print(f"{t1 - t0:.6f} {ru.ru_maxrss}")
sys.exit(0 if p.returncode == 0 else 1)
' "$bin"
}
# Run a binary RUNS times, drop the slowest run by wall time, return
# median of the rest as `wall rss` (rss = max across the kept runs).
median_run() {
local bin="$1"
local times=()
local rsses=()
for ((i = 0; i < RUNS; i++)); do
read -r w r < <(time_one "$bin")
times+=("$w")
rsses+=("$r")
done
# Compute index of slowest (largest wall) and drop it.
local slowest_idx=0
for ((i = 1; i < ${#times[@]}; i++)); do
if [[ $(awk -v a="${times[$i]}" -v b="${times[$slowest_idx]}" 'BEGIN { print (a > b) ? 1 : 0 }') == 1 ]]; then
slowest_idx=$i
fi
done
local kept_t=()
local kept_r=()
for ((i = 0; i < ${#times[@]}; i++)); do
if [[ $i -ne $slowest_idx ]]; then
kept_t+=("${times[$i]}")
kept_r+=("${rsses[$i]}")
fi
done
# Median wall over kept runs.
local sorted_t
sorted_t=$(printf "%s\n" "${kept_t[@]}" | sort -g)
local n=${#kept_t[@]}
local mid=$((n / 2))
local median_t
if (( n % 2 == 1 )); then
median_t=$(echo "$sorted_t" | sed -n "$((mid + 1))p")
else
local a b
a=$(echo "$sorted_t" | sed -n "${mid}p")
b=$(echo "$sorted_t" | sed -n "$((mid + 1))p")
median_t=$(awk -v a="$a" -v b="$b" 'BEGIN { printf "%.6f", (a + b) / 2 }')
fi
# Max RSS across kept runs (peak memory is the natural per-run agg).
local max_r=0
for r in "${kept_r[@]}"; do
if (( r > max_r )); then max_r=$r; fi
done
printf "%s %s\n" "$median_t" "$max_r"
}
echo
echo ">>> timing (RUNS=$RUNS, drop slowest, median of $((RUNS - 1)))"
echo
# Header. The rc/bump ratio is the RC-overhead-vs-bump bench-health
# regression gate (1.3× ceiling on linear/tree corpus, ±15% on
# closure-chain).
printf "%-22s | %10s | %10s | %10s | %12s | %12s\n" \
"workload" "bump(s)" "rc(s)" "rc/bump" "bump RSS(KB)" "rc RSS(KB)"
printf -- "-----------------------+------------+------------+------------+--------------+--------------\n"
for f in "${fixtures[@]}"; do
read -r bp_t bp_r < <(median_run "$OUTDIR/${f}_bump")
read -r rc_t rc_r < <(median_run "$OUTDIR/${f}_rc")
# Guard against bump_t == 0 (LLVM-folded sub-microsecond fixtures).
rc_ratio=$(awk -v r="$rc_t" -v b="$bp_t" 'BEGIN { if (b+0 == 0) printf "n/a"; else printf "%.2fx", r / b }')
printf "%-22s | %10s | %10s | %10s | %12s | %12s\n" \
"$f" "$bp_t" "$rc_t" "$rc_ratio" "$bp_r" "$rc_r"
done
# Latency bench. The throughput table above is wall-time-and-RSS;
# `bench/latency_harness.py` measures per-operation tail latency
# (median + p99 + p99.9 + max) on PTY-line-buffered stdout for the
# `bench_latency_*` fixtures. We invoke it for the two RC arms
# (RC-fair explicit @ rc, implicit-mode @ rc as control) and emit a
# second table.
#
# Skipped if the harness / fixtures aren't present (the latency bench
# was added in 18f.2 and may not exist on older branches that share
# this script).
LAT_HARNESS="$ROOT/bench/latency_harness.py"
LAT_IMPL_SRC="$ROOT/examples/bench_latency_implicit.ail"
LAT_EXPL_SRC="$ROOT/examples/bench_latency_explicit.ail"
if [[ -x "$LAT_HARNESS" && -f "$LAT_IMPL_SRC" && -f "$LAT_EXPL_SRC" ]]; then
echo
echo ">>> latency bench (PTY inter-arrival, 1000 samples per arm)"
echo
# Build the two RC arms. -O2 to match the throughput table.
"$AIL" build --opt=-O2 --alloc=rc "$LAT_EXPL_SRC" -o "$OUTDIR/bench_latency_explicit_rc" >/dev/null
"$AIL" build --opt=-O2 --alloc=rc "$LAT_IMPL_SRC" -o "$OUTDIR/bench_latency_implicit_rc" >/dev/null
# The harness prints a multi-line block per arm; we let it speak
# for itself. The orchestrator captures the verbatim output into
# the commit body like the throughput table above. `--runs 5` runs
# each arm five times; the harness drops the slowest run and
# reports median + range per cell, matching the throughput
# table's drop-slowest convention.
"$PY" "$LAT_HARNESS" "$OUTDIR/bench_latency_explicit_rc" --runs 5 --label "explicit @ rc (RC-fair)"
echo
"$PY" "$LAT_HARNESS" "$OUTDIR/bench_latency_implicit_rc" --runs 5 --label "implicit @ rc (control)"
fi
echo
echo ">>> done"