113 Commits

Author SHA1 Message Date
Brummel 4cacfcbdac bench: mono-vs-vdisp micro-benchmark + revised Decision 11 rationale
Hypothesis-driven measurement of "did monomorphisation actually
buy us performance?" on a 100M-iter LCG hot loop, AILang mono'd
code vs. four C reference variants (direct-inlinable, direct-
noinline, indirect-monomorphic, indirect-polymorphic). Zen 3,
clang -O2, median-of-15.

Headline: H1 supported, but the mechanism is inlining, not
dispatch shape. AILang mono = hand-C direct (1.000x). Indirect-
monomorphic = direct-noinline (1.000x) — saturating branch
predictor makes the indirect-call cost vanish on this hardware.
Inlining is the actual 3.31x win; polymorphic indirect adds
another 21% predictor-miss penalty.

DESIGN.md Decision 11 gains a rationale paragraph reframing mono
as inlining-enabler rather than indirect-call-eliminator, with
explicit pointer to the bench. JOURNAL entry records the full
methodology, ratios, limitations, and the side-effect mono-pass
env.globals-seeding bug surfaced while building the AILang fixture
(separate RED-first debug iter to follow).
2026-05-10 01:03:21 +02:00
Brummel aea3758742 tidy: 21'g — resolve drift after 21'-arc close
Three drift items from ailang-architect, plus one false-positive
surfaced during verification:

  1. DESIGN.md silent on closure-pair 4.14x finding (21'b).
     Decision-10 ratified: "Workload scope of the 1.3x target"
     paragraph scopes the retirement gate to linear/tree/poly-ADT
     workloads; closure-pair carve-out documented as
     representational cost (closure cell + env struct = 2 allocs
     per step) until a slab/pool answer ships.

  2. bench/compile_check.py corpus drift. Three fixtures added
     (bench_compute_intsum, bench_compute_collatz,
     bench_list_sum_explicit), re-baselined. Now 12 fixtures x
     2 ops = 24 compile-time metrics.

  3. baseline.json convention not codified. Note field gains
     "max-of-distribution gets wider band than percentile"
     convention discovered in 21'd.

  4. (verification finding) bench_compute_intsum cross_lang
     tolerances at 15%/12% fire on subprocess-spawn jitter for
     sub-millisecond fixtures. Widened to 35% across all five
     intsum metrics; convention recorded in baseline_cross_lang
     note field (sub-ms fixtures need looser bands).

All three bench gates re-run sequentially after edits:
  bench/check.py        — 63 metrics; 63 stable
  bench/compile_check.py — 24 metrics; 24 stable
  bench/cross_lang.py    — 25 metrics; 25 stable

Total: 112 metrics under regression coverage, all green.

288 tests passing, 3 ignored. No Rust changes.
2026-05-09 01:40:36 +02:00
Brummel 75f7fda788 bench: 21'f — explicit-mode pair, full alloc+dec vs malloc+free
Closes the apples-to-apples gap from 21'e. Adds:
- examples/bench_list_sum_explicit.ailx — same algorithm and sizes
  as bench_list_sum, fully (borrow)/(own)/(drop-iterative)
  annotated so codegen emits proper inc/dec instrumentation.
- bench/reference/list_sum_explicit_free.c — same algorithm
  with explicit free() walking the chain after sum.

The full alloc+dec vs malloc+free comparison reveals two non-
trivial conclusions:

1. AILang's full RC pipeline is only 26% slower than glibc
   malloc+free on this workload (rc/c = 1.26x). The implicit-
   mode comparison's 1.42x was misleading — it counted neither
   pipeline's free path. The fair ratio is 1.26x, materially
   better than the previous read.

2. RC's dec is cheaper per cell than glibc free(). AILang
   dec-tax: ~3 ns/cell. C free-tax: ~5.5 ns/cell. Plausible
   cause: ailang_rc_dec operates on a known-shape cell with a
   fixed-offset refcount and a static per-type drop fn — no
   free-list bucketing, no header introspection, no global lock.

bump's advantage expresses fully: bench_list_sum_explicit.bump/c
= 0.42x means AILang at bump is 2.4x faster than C malloc+free.
Sets a useful upper bound on a slab/pool RC allocator's potential.

The 21'-family arc — bench-regression infrastructure — is now
substantively complete: 21'a (bench/check.py), 21'b (corpus
widening), 21'c (compile_check.py), 21'd (pure-compute fixtures
+ harness hardening), 21'e (cross-language hand-C), 21'f (explicit
apples-to-apples). 63 runtime metrics + 18 compile metrics + 25
cross-lang metrics under regression coverage. Any future iter
that regresses any axis beyond tolerance gets caught at the next
family close.

Remaining queue is back to substantive language work — Family 21
(typeclasses / polymorphic ADTs at runtime / pattern-binding
generalisation) is now an orchestrator-level fork that needs
direct user input.
2026-05-09 01:21:15 +02:00
Brummel c897d2eef0 bench: 21'e — cross-language reference, AILang/C ratios
Closes the question CLAUDE.md has carried since day one ("LLVM-
linkable, performance is extremely important") with data. Hand-C
variants of the four bench fixtures, compiled with clang -O2,
each carefully matching the AILang algorithm and explicitly
documenting representation differences (cell width, leak policy)
that affect the ratio.

Three substantive findings:

1. Pure-compute parity with C: bench_compute_collatz runs at
   AILang/C = 0.99x across both allocators. AILang's IR composes
   with LLVM's optimizer at the same level a hand-C source does.
   This is the LLVM-linkable performance claim, backed by data
   for the first time. bench_compute_intsum (1.05-1.18x) confirms.

2. AILang bump beats glibc malloc 2x on linear allocation:
   bench_list_sum.bump/c = 0.50x. Bump's two-instruction inline
   fastpath outperforms glibc's free-list-managed malloc on
   no-free workloads. Quantitatively measured for the first time.

3. RC overhead vs C malloc quantified: bench_list_sum.rc/c =
   1.49x, bench_tree_walk.rc/c = 2.61x. The 8-byte refcount
   header + zero-init + libc backing add 50-160% over glibc
   malloc on these implicit-mode workloads. Explicit-mode + a
   free()-adding C variant (21'f, queued) will close the
   apples-to-apples gap on dec-cost.

CLAUDE.md updated to list bench/cross_lang.py as the third
tidy-iter gate alongside bench/check.py and bench/compile_check.py.
20 new metrics in bench/baseline_cross_lang.json with 12-15%
tolerances (cross-language ratios are inherently noisier than
within-AILang ratios — two compiler stacks contribute variance).
2026-05-09 01:15:37 +02:00
Brummel 5a4a6de031 bench: 21'd — pure-compute fixtures + harness hardening
Closes the third corpus blind spot (heap-allocation-only) by
adding two fixtures with no allocation pressure: bench_compute_
intsum (tail-recursive integer accumulator) and bench_compute_
collatz (Collatz step-counter, branchy).

Surprise on intsum: 50M-iteration loop runs in 1ms wall under
all three allocators. LLVM's induction-variable analysis applies
the closed-form triangular-sum reduction to AILang's IR — a
positive codegen finding (the IR composes with LLVM's optimizer
at the same level a hand-C loop would) but it makes intsum
useless as a runtime regression bench. Excluded from run.sh's
fixtures array; kept in examples/ as reference and as a future
cross-language comparison anchor.

Collatz survives optimization (data-dependent control flow). At
56ms wall, gc/bump/rc all within 2% — the canonical "pure-compute
is allocator-invariant" data point this fixture is meant to
prove. If a future codegen change leaks an allocation into the
inner loop, the 1.00x / 1.02x ratios diverge visibly.

Two infrastructure fixes the new fixtures forced:
- 6-decimal precision in run.sh's Python timing helper and median
  averager (was 3-decimal; sub-ms times rounded to 0.000 and
  crashed the ratio awk with Division durch Null).
- Zero-guard in the ratio awk (defensive even with the precision
  bump, since LLVM-eliminated workloads can still hit zero).

Latency baseline: implicit_at_rc.max_us tolerance 25% -> 30%.
Three captures today (477 / 456 / 609 µs) show natural run-to-run
dispersion wider than the original tolerance accounts for. Not a
softening to dodge regression — the original baseline was the
first capture; a fairer tolerance across natural max-of-1000-
samples width is what the harness needed from the start.

Baseline file: 47 -> 55 metrics. 21'e (cross-language reference,
clang -O2 hand-C ratios) is the natural next dispatch.
2026-05-09 01:11:26 +02:00
Brummel 416d763b73 bench: 21'c — compile-time regression bench (check.py + compile_check.py)
Closes the second axis the user named: every typechecker / codegen
perf change was previously invisible to the tidy-iter gate. With
Family 21 typeclasses (queued) and 21'b's poly-ADT additions both
pushing on the typechecker, naive substitution loops would have
landed silently and decayed the compile path.

bench/compile_check.py is a separate script from bench/check.py
because the methodology differs: sub-process spawn timing on small
workloads (1ms scale for `ail check`, 65ms for `ail build`) vs.
allocator-stress on large ones (multi-second). Tolerances differ
by an order of magnitude (25% / 20% here vs. 5-15% there).

Empirically: ail check is sub-ms across the corpus, dominated by
subprocess spawn (~5-10ms on Linux); ail build is 63-69ms,
dominated by clang's link step. The bench is a catastrophe
detector (10x slowdowns visible) — finer regressions need a
profiler. CLAUDE.md updated to list both scripts as co-equal
tidy-iter gates alongside the architect drift report; exit 0/1/2
semantics are uniform across both.

JOURNAL queue: 21'd (pure-compute fixtures) and 21'e (cross-
language reference / hand-C ratio) remain to land the LLVM-
linkable performance claim.
2026-05-09 00:59:00 +02:00
Brummel 07bff24527 bench: 21'b — closure-pair + HOF/poly fixtures, 47-metric baseline
Two new throughput fixtures targeting blind spots in the 21'a
corpus:

- bench_closure_chain exercises the build_pair_drop_fn codegen
  path (the 18c.4 doubled-braces trigger). Each iteration of
  run_loop allocates a {thunk, env} closure pair via the
  let-rec-name-as-value escape route. Sizes 10k / 100k / 500k.
  rc/bump = 4.14x — materially worse than the 2.91x / 2.59x of
  the linear/tree fixtures, exposing that closure work pays the
  RC alloc tax twice (pair + env-struct).

- bench_hof_pipeline exercises poly-ADT instantiation and
  indirect dispatch via fold_with_fn over List<a>. Sizes 100k /
  1M / 3M elements. Ratios essentially match bench_list_sum,
  confirming the 13b static-template-plus-ctor-inline design
  has zero measurable overhead at this scale.

Baseline file extends from 31 to 47 metrics. The two new fixtures
build clean under all three allocators; the rc-arm build exercises
the per-type drop fn for the closure-pair, providing a tripwire
for any future 18c.4-class IR malformedness.

JOURNAL records both surprises (4.14x closure tax, ~zero HOF/poly
overhead) and explicitly notes the dispersion observation on
explicit_at_rc.p99 — three captures today (357.5 / 294.6 / 251.5)
confirm wide run-to-run variance on that fixture. Methodology
upgrade (n>=10 captures or tighter fixture) deferred to 21'c.

bench/run.sh fixtures array updated. bench/check.py needed no
changes — its parser handles the wider table by metric name.
2026-05-09 00:55:57 +02:00
Brummel a1b0ad5723 bench: regression harness — baseline.json + check.py
Closes the structural gap between bench/run.sh (one-shot capture
into JOURNAL) and a continuous tripwire. baseline.json records 31
metrics (16 throughput, 15 latency) with per-metric one-sided
tolerances tuned to absorb run-to-run noise on a quiet developer
machine; check.py spawns run.sh, parses both the throughput
pipe-table and the per-arm latency stanzas, diffs against the
baseline, prints a per-metric report, and exits non-zero on any
regression beyond tolerance.

User-facing flags: --from-file, --stdin, --baseline, and
--update-baseline (re-run + overwrite baseline.json after
intentional improvements).

Validation: captured the baseline, then re-ran end-to-end. All 31
metrics within tolerance. The harness also caught a single-capture
explicit-rc p99 spike (357.5 us) that was first read as drift vs.
yesterday's 18g.tidy.fu2 numbers but came in at 294.6 us on the
follow-up run — exactly the kind of noise the tolerance band is
there to absorb. Without 21'a we would have either chased a
phantom or buried the signal; with it, single noisy runs are data
points, not verdicts.

Tidy-iter discipline addition (run check.py at every family close
alongside the architect drift report) is recommended in JOURNAL
but not enacted in this iter — that's an orchestrator-level
update to CLAUDE.md.
2026-05-09 00:36:06 +02:00
Brummel c2af5ad3da bench: latency_harness --runs N (stat-of-N, drop-slowest)
Closes the architect's tidy follow-up item on bench-number
methodology. The harness now accepts --runs N (default 1, byte-
identical output for back-compat); with N>=2 it reports
median + min..max per cell across runs, with N>=4 it drops the
slowest run before aggregating to match bench/run.sh's drop-
slowest throughput convention.

bench/run.sh now invokes the harness with --runs 5 for each of
the three latency arms, so a single bench/run.sh run produces
both a regression-stable throughput table and a regression-stable
latency table.

The qualitative claims from JOURNAL 2026-05-08 (RC tail latency
23x better than Boehm; RC RSS lower than Boehm) hold at the new
stat-of-5 confidence level — a 5-run smoke on the explicit-rc arm
shows p99=296µs (range 289-311), p99/median=1.31x (range 1.28-1.37).
Variance is well below the signal.
2026-05-08 15:17:53 +02:00
Brummel 3113258680 tidy: 18g sub-arc — DESIGN ratification, latency bench, negative coverage
Resolves the architect's drift report on the 18g sub-arc:

- DESIGN.md: ratify mode-metadata's codegen role. param_modes /
  ret_mode were already on Type::Fn since 18a; with 18d.4 / 18g
  they became load-bearing for drop-emission decisions in
  codegen. The new 'Mode metadata is load-bearing for codegen'
  subsection records the four seams (Iter A + Iter B + 18g.1 +
  18g.2) and names the let-alias-of-borrow carve-out the gates
  do not propagate through.

- bench/run.sh: post-throughput, invoke bench/latency_harness.py
  on the three canonical arms (Implicit @ gc, explicit @ rc,
  Implicit @ rc control). The Boehm-retirement bench numbers
  are now reproducible by anyone running the harness, not just
  by hand on a specific host.

- Negative coverage: examples/rc_let_implicit_returning_app
  + alloc_rc_let_binder_for_implicit_returning_app_does_not_drop
  pin the asymmetry to the (own)-ret-mode test (live=0 there,
  live=1 here). The Borrow-ret-mode case is covered by the
  language design itself — typechecker rejects 'borrow-
  passthrough' shapes with consume-while-borrowed.

JOURNAL entry records four items as deferred known debt:
emit_inlined_partial_drop dynamic-tag fallback, carve-out
diagnostics surface, bench-number stat-of-N, and the
cross-family ordering observation about CLAUDE.md's tidy-iter
rule.

The 18-arc is formally closed with this tidy. Next iter is
the orchestrator's Boehm-retirement decision (Path A vs Path B
from JOURNAL 2026-05-08 18f entry, joined by the post-18g.2
re-bench numbers).
2026-05-08 14:37:36 +02:00
Brummel ac70011352 bench: latency harness + paired latency fixtures
The latency_harness.py harness spawns the bench binary on a PTY,
records monotonic_ns() per stdout line, and reports inter-arrival
gap distribution (median / p99 / p99.9 / max). Tail latency is
Decision 10's central real-time claim; total wall-time and RSS are
the wrong metrics for that question.

Paired fixtures: bench_latency_implicit (Boehm-fair, no mode
annotations, leaks under --alloc=rc) and bench_latency_explicit
(mode-annotated hot path, what RC was built for). Both use a
depth-19 balanced tree (~16 MB) as the persistent live working
set, plus per-op IntList build+sum churn forcing GC pressure
under Boehm.

Authored by ailang-bencher; ships evidence, not features.
2026-05-08 14:05:03 +02:00
Brummel 234dbb6c5a bench: extend run.sh with --alloc=rc column for Iter 18f
Adds rc to the modes list and an rc/bump ratio column in the
results table. The rc/bump ratio is the decisive number for
Decision 10's Boehm-retirement threshold (target 1.3x).
2026-05-08 12:56:53 +02:00
Brummel 65e280bb70 Bench: GC overhead via bump-allocator comparison
Adds --alloc=<gc|bump> to ail build/run. Bump path links a 256MB
no-free arena C stub instead of libgc; IR is byte-identical except
for the @GC_malloc → @bump_malloc symbol swap. Bench harness times
two allocation-heavy workloads (list cons/sum and balanced tree
build/walk) under both modes.

Numbers (RUNS=5, median of 4):
  bench_list_sum   gc 0.141s  bump 0.048s  +194%
  bench_tree_walk  gc 0.103s  bump 0.041s  +151%

Bucket: large. ~60% of runtime is Boehm on these workloads —
upper bound for any realistic program. Both fixtures hold the
heap fully live, so the cost we're seeing is Boehm's allocate
path itself, not collection work; that fact narrows the design
space for the GC discussion.

- crates/ailang-codegen: AllocStrategy enum, three callsites and
  the IR header parameterised.
- crates/ail/src/main.rs: --alloc flag plumbed; bump runtime
  located + compiled on demand.
- runtime/bump.c: 256MB static arena, abort-on-overflow.
- examples/bench_list_sum, bench_tree_walk: accumulator-form
  fixtures (textbook recursive sum was constructor-blocked).
- bench/run.sh: harness with Python timing helper (Arch's
  /usr/bin/time isn't part of the base install).

No language-level changes; default --alloc=gc, all 141 workspace
tests green, all 5 IR snapshots unchanged, 11 prior fixtures
produce identical stdout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:06:20 +02:00