Files
AILang/bench/cross_lang.py
T
Brummel 9fdc4cacff iter form-a.1 (Tasks 6-12): milestone close
Second half of the form-a-default-authoring milestone-close iter
(Boss-decided strategy C, big-bang). All seven tasks DONE; cargo
test --workspace green at every per-task boundary.

T6 — Bench-driver suffix flip from .ail.json to .ail across 4
Python scripts + run.sh. compile_check.py + cross_lang.py exit 0.

T7 — Re-author e2e.rs raw-JSON-inspect tests:
- diff_detects_changed_def — derive sum.ail.json on-the-fly via
  `ail parse examples/sum.ail` into tempdir, then mutate + diff.
- borrow_own_demo_modes_are_metadata_only — same pattern.
- reuse_as_demo_under_rc_uses_inplace_rewrite — same pattern.
- render_parse_round_trip_canonical — RETIRED (subsumed by T1's
  cli_parse_then_render_then_parse_is_idempotent over whole corpus).
- ail_run_accepts_ail_source_with_same_stdout_as_ail_json —
  re-authored to derive hello.ail.json in a per-process tempdir
  from hello.ail via `ail parse`, then assert dual-form stdout.

T8 — Bulk-delete 156 non-carve-out .ail.json. Inventory:
8 .ail.json (carve-outs, alphabetical: broken_unbound + prelude
+ 3× test_22b2_* + 3× test_ct1_*) + 157 .ail. carve_out_inventory
test un-#[ignore]'d and green. Forward-pulled 20 repairs that the
T1-5 dispatch's recon missed (12 Group-B suffix + 5 Group-A
load_workspace + 3 ail_run sites). Also forward-pulled T9 Step 5
(schema_coverage corpus flip from .ail.json to .ail) to satisfy
T8's green-gate.

T9 — Retire obsolete roundtrip tests:
- print_then_parse_round_trips_every_fixture (round_trip.rs)
- every_ail_fixture_matches_its_json_counterpart (round_trip.rs)
- cli_render_then_parse_preserves_canonical_bytes_on_every_fixture
- Dead helpers: list_json_fixtures ×2, round_trip_one,
  strip_trailing_newlines.
Schema-coverage corpus already flipped in T8 (forward-pull).

T10 — DESIGN.md §"Roundtrip Invariant" (lines 2027-2109) restated
with parse-determinism + idempotency + CLI-pipeline-idempotency +
carve-out-anchor framing. Five surviving enforcement tests named.
§"Float literals" and §"Why anchored at top level" preserved.

T11 — §A4 doctrine edits: CLAUDE.md:5-6 + DESIGN.md:465-466.
Canonical form remains JSON-AST; authoring projection is .ail;
build derives JSON-AST in-process via ailang_surface::parse.

T12 — Milestone close:
- WhatsNew entry: user-facing language, lead with the change.
- Roadmap: [milestone] form-a struck [x] with closing note.
- Final inventory verified: 8 .ail.json + 157 .ail.
- Final cargo test --workspace: 557 passed, 0 failed, 3 ignored.
- bench/compile_check.py + bench/cross_lang.py: exit 0.

Test math: pre-iter 558 baseline + 3 new T1 tests = 561, − 1
(T7 retire) − 3 (T9 retire) = 557 final.

INDEX.md appended with the full iter summary covering T1-T12 (the
T1-5 commit at 77b28ad deferred the INDEX line to full-iter close).

Milestone [Form-A as the default authoring surface] structurally
closed. The compile-time-embed carve-out (prelude.ail.json) is
the subject of the queued follow-up milestone [Prelude embed:
Form-A as compile-time source]. audit-form-a runs as the next
dispatch.
2026-05-13 11:31:39 +02:00

243 lines
8.8 KiB
Python
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 python3
# Cross-language reference bench.
#
# For each fixture in CORPUS, builds:
# - AILang binary at -O2 --alloc=rc (canonical default)
# - AILang binary at -O2 --alloc=bump (no-free upper bound)
# - hand-C binary via clang -O2 from bench/reference/<fixture>.c
# Times each, drops the slowest of N runs, takes the median, computes
# AILang/C ratios. Diffs against bench/baseline_cross_lang.json or, with
# --update-baseline, captures fresh numbers as the new baseline.
#
# What this answers (and what it does not): the AILang/C ratio is the
# honest answer to CLAUDE.md's "LLVM-linkable, performance is extremely
# important" claim. Ratios near 1.0 mean AILang's IR composes with
# LLVM's optimizer at the same level as a hand-C source; ratios of
# 2-3x are JIT-quality territory; ratios of 10x+ are a real problem.
# The bench DOES NOT distinguish "AILang's IR is slow" from "the
# discriminated-union representation is wider than C's hand-tuned
# struct"; that's a representation question, not an optimizer one.
# Each fixture's .c file documents its representation choices so the
# orchestrator can read the ratio with that context in mind.
#
# Usage:
# bench/cross_lang.py # run + diff
# bench/cross_lang.py -n 10 # tighter median
# bench/cross_lang.py --update-baseline
# bench/cross_lang.py --baseline path
from __future__ import annotations
import argparse
import json
import os
import resource
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_BASELINE = ROOT / "bench" / "baseline_cross_lang.json"
AIL = ROOT / "target" / "release" / "ail"
EXAMPLES = ROOT / "examples"
REFERENCE = ROOT / "bench" / "reference"
OUTDIR = ROOT / "target" / "cross_lang"
# (ailang fixture stem, C reference stem) — both must exist as
# `examples/<ail>.ail` and `bench/reference/<c>.c`.
CORPUS = [
("bench_list_sum", "list_sum"),
("bench_tree_walk", "tree_walk"),
("bench_compute_intsum", "compute_intsum"),
("bench_compute_collatz", "compute_collatz"),
("bench_list_sum_explicit", "list_sum_explicit_free"),
]
def time_one(bin_path: Path) -> float:
"""Run binary, return wall-time in seconds. Stdout discarded."""
t0 = time.monotonic()
proc = subprocess.run([str(bin_path)], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
t1 = time.monotonic()
if proc.returncode != 0:
raise RuntimeError(f"{bin_path} exited {proc.returncode}")
return t1 - t0
def median_drop_slowest(runs: list[float]) -> float:
if len(runs) < 2:
return runs[0] if runs else 0.0
kept = sorted(runs)[:-1]
n = len(kept)
if n % 2 == 1:
return kept[n // 2]
return 0.5 * (kept[n // 2 - 1] + kept[n // 2])
def build_ailang(stem: str, alloc: str) -> Path:
src = EXAMPLES / f"{stem}.ail"
if not src.is_file():
print(f"missing AILang fixture: {src}", file=sys.stderr)
sys.exit(2)
bin_path = OUTDIR / f"{stem}_{alloc}"
subprocess.run(
[str(AIL), "build", "--opt=-O2", f"--alloc={alloc}", str(src), "-o", str(bin_path)],
check=True, capture_output=True,
)
return bin_path
def build_c(stem: str) -> Path:
src = REFERENCE / f"{stem}.c"
if not src.is_file():
print(f"missing C reference: {src}", file=sys.stderr)
sys.exit(2)
bin_path = OUTDIR / f"{stem}_c"
subprocess.run(["clang", "-O2", "-o", str(bin_path), str(src)],
check=True, capture_output=True)
return bin_path
def measure(num_runs: int) -> dict[str, dict[str, float]]:
if not AIL.is_file():
subprocess.run(["cargo", "build", "--release", "-p", "ail"],
cwd=str(ROOT), check=True, capture_output=True)
OUTDIR.mkdir(parents=True, exist_ok=True)
out: dict[str, dict[str, float]] = {}
for ail_stem, c_stem in CORPUS:
print(f">>> {ail_stem}", file=sys.stderr)
ail_rc = build_ailang(ail_stem, "rc")
ail_bump = build_ailang(ail_stem, "bump")
c_bin = build_c(c_stem)
rc_runs = [time_one(ail_rc) for _ in range(num_runs)]
bump_runs = [time_one(ail_bump) for _ in range(num_runs)]
c_runs = [time_one(c_bin) for _ in range(num_runs)]
rc_s = median_drop_slowest(rc_runs)
bump_s = median_drop_slowest(bump_runs)
c_s = median_drop_slowest(c_runs)
out[ail_stem] = {
"ail_rc_s": rc_s,
"ail_bump_s": bump_s,
"c_s": c_s,
"rc_over_c": (rc_s / c_s) if c_s > 0 else 0.0,
"bump_over_c": (bump_s / c_s) if c_s > 0 else 0.0,
}
print(f" ail_rc={rc_s*1000:7.2f}ms ail_bump={bump_s*1000:7.2f}ms "
f"c={c_s*1000:7.2f}ms rc/c={out[ail_stem]['rc_over_c']:5.2f}× "
f"bump/c={out[ail_stem]['bump_over_c']:5.2f}×", file=sys.stderr)
return out
def diff_report(measured: dict, baseline: dict) -> tuple[str, bool]:
rows = []
has_regression = False
for fixture, spec_dict in baseline.get("fixtures", {}).items():
actual = measured.get(fixture)
if actual is None:
continue
for metric, spec in spec_dict.items():
base = spec["baseline"]
tol = spec["tolerance_pct"]
a = actual.get(metric)
if a is None:
continue
diff = 100.0 * (a - base) / base if base else 0.0
if diff > tol:
status = "REGRESSION"
has_regression = True
elif diff < -tol:
status = "improvement"
else:
status = "ok"
rows.append((f"{fixture}.{metric}", base, a, diff, tol, status))
lines = []
lines.append(f"{'metric':<48} {'baseline':>10} {'actual':>10} {'diff':>9} {'tol':>6} status")
lines.append("-" * 100)
for m, b, a, d, t, s in rows:
lines.append(f"{m:<48} {b:>10.4f} {a:>10.4f} {d:>+8.2f}% {t:>5.1f}% {s}")
regressed = sum(1 for r in rows if r[5] == "REGRESSION")
improved = sum(1 for r in rows if r[5] == "improvement")
stable = len(rows) - regressed - improved
lines.append("")
lines.append(f"summary: {len(rows)} metrics; "
f"{regressed} regressed, {improved} improved beyond tolerance, "
f"{stable} stable")
return "\n".join(lines), has_regression
def write_baseline(measured: dict, path: Path) -> None:
today = subprocess.check_output(["date", "+%Y-%m-%d"], text=True).strip()
if path.exists():
existing = json.loads(path.read_text())
existing_tols = existing.get("fixtures", {})
else:
existing_tols = {}
DEFAULT_TOLS = {
"ail_rc_s": 15,
"ail_bump_s": 15,
"c_s": 15,
"rc_over_c": 12,
"bump_over_c": 12,
}
new = {
"version": 1,
"captured": today,
"captured_via": "bench/cross_lang.py",
"note": "Cross-language wall-time baseline. Per-fixture: AILang at --alloc=rc, AILang at --alloc=bump, hand-C at clang -O2. Ratios rc/c and bump/c are the headline answer to CLAUDE.md's LLVM-linkable performance claim. The C reference uses malloc-and-leak to mirror AILang's implicit-mode RC; an explicit-mode + free() variant is queued.",
"fixtures": {},
}
for fixture, metrics in measured.items():
existing_fix = existing_tols.get(fixture, {})
new["fixtures"][fixture] = {
metric: {
"baseline": round(value, 6),
"tolerance_pct": existing_fix.get(metric, {}).get(
"tolerance_pct", DEFAULT_TOLS.get(metric, 15)
),
}
for metric, value in metrics.items()
}
path.write_text(json.dumps(new, indent=2) + "\n")
print(f">>> wrote new baseline to {path}", file=sys.stderr)
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("-n", "--runs", type=int, default=5,
help="runs per binary; min 2, default 5")
ap.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE)
ap.add_argument("--update-baseline", action="store_true")
args = ap.parse_args()
if args.runs < 2:
print("--runs must be >= 2", file=sys.stderr)
return 2
measured = measure(args.runs)
if args.update_baseline:
write_baseline(measured, args.baseline)
return 0
if not args.baseline.exists():
print(f"no baseline at {args.baseline}; create one with --update-baseline",
file=sys.stderr)
return 2
baseline = json.loads(args.baseline.read_text())
report, has_regression = diff_report(measured, baseline)
print(report)
return 1 if has_regression else 0
if __name__ == "__main__":
sys.exit(main())