iter ext-rename: .ailx → .ail across the live toolchain

The surface-form file extension changes from .ailx to .ail. AILang's
authoring surface now uses the same .ail stem as its canonical JSON
form (.ail.json), giving the language a single coherent extension
family: .ail is the LLM-authored Form A, .ail.json is the canonical
JSON-AST Form B.

Scope (touched):
- 61 example renames examples/**/*.ailx → .ail (git mv)
- 1 rename experiments/.../rendered/ailx.md → ail.md
- 35 content-edited live-toolchain files (crates/, docs/DESIGN.md,
  docs/roadmap.md, docs/PROSE_ROUNDTRIP.md, skills/, bench/reference/*.c,
  experiment crates under experiments/.../{render,harness,master})
- Experiment-crate cohort rename Cohort::Ailx → Cohort::Ail,
  Form::Ailx → Form::Ail, per_cohort/ailx → per_cohort/ail,
  {form-only: ailx} → {form-only: ail}, ```ailx → ```ail

Out of scope (deliberately untouched, to preserve honest history):
- docs/journal-archive.md (content-frozen per CLAUDE.md)
- docs/journals/, docs/specs/, docs/plans/, bench/orchestrator-stats/
- experiments/.../runs/ (frozen LLM-output artefacts; models actually
  saw .ailx — renaming would falsify the experimental record)

Verification: cargo build/test --workspace green; experiment crate
cargo test green; bench/check.py + compile_check.py + cross_lang.py
all 0-regressed; negative grep for ailx|Ailx|AILX outside the
out-of-scope paths returns zero matches.

Opens immediate follow-up: roadmap.md P2 todo `ail check`/build/run
accept .ail extension — after this rename, .ail is canonical
authoring surface but the CLI still produces a misleading JSON-parse
error on `ail check foo.ail`. That's the next iter.
This commit is contained in:
2026-05-12 14:20:27 +02:00
parent 17b370bbb3
commit 72e54f4fd3
97 changed files with 318 additions and 210 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
// Hand-C reference for bench_compute_collatz.
//
// Same algorithm as examples/bench_compute_collatz.ailx — for each
// Same algorithm as examples/bench_compute_collatz.ail — for each
// starting value in [1..N], count Collatz steps to reach 1, sum.
// Three sizes: 10k / 100k / 500k starting values.
//
+1 -1
View File
@@ -1,6 +1,6 @@
// Hand-C reference for bench_compute_intsum.
//
// Same algorithm as examples/bench_compute_intsum.ailx — accumulate
// Same algorithm as examples/bench_compute_intsum.ail — accumulate
// `i * 7` for i in [n, n-1, ..., 1], printing the final acc.
// Three sizes: 1M / 10M / 50M iterations.
//
+1 -1
View File
@@ -1,6 +1,6 @@
// Hand-C reference for bench_list_sum.
//
// Same algorithm as examples/bench_list_sum.ailx — build a linked list
// Same algorithm as examples/bench_list_sum.ail — build a linked list
// of [0, 1, ..., N-1] via prepending, then sum by linear traversal.
// Three workload sizes: 100k / 1M / 3M cells, matching the AILang
// fixture exactly so the AILang/C wall-time ratio is fair.
+1 -1
View File
@@ -2,7 +2,7 @@
//
// Companion to list_sum.c (which leaks); this version adds explicit
// `free()` calls to mirror AILang's explicit-mode RC dec-tax. Pairs
// with examples/bench_list_sum_explicit.ailx via bench/cross_lang.py.
// with examples/bench_list_sum_explicit.ail via bench/cross_lang.py.
//
// The free pass walks the list after sum, freeing each cell. This is
// genuinely O(N) work that bench_list_sum.c never paid; the rc/c
+1 -1
View File
@@ -1,6 +1,6 @@
// Hand-C reference for bench_tree_walk.
//
// Same algorithm as examples/bench_tree_walk.ailx — build a balanced
// Same algorithm as examples/bench_tree_walk.ail — build a balanced
// binary tree of given depth (every value = 1) and sum every node.
// Three depths: 16 / 18 / 20 (= 65535 / 262143 / 1048575 nodes).
//
+3 -3
View File
@@ -190,7 +190,7 @@ enum Cmd {
#[arg(long)]
json: bool,
},
/// Parses a `.ailx` source file (form (A)) into canonical
/// Parses a `.ail` source file (form (A)) into canonical
/// `.ail.json`. Iter 14c addition; symmetric to `render`.
///
/// The form-(A) projection is one of potentially many producers of
@@ -269,7 +269,7 @@ fn compose_merge_prose_prompt(original_form_a: &str, edited_prose: &str) -> Stri
"You are integrating prose edits back into an AILang module.
ROLE
Your job is to produce an updated AILang module in Form-A (an .ailx
Your job is to produce an updated AILang module in Form-A (an .ail
file) that reflects the human's prose edits while preserving the
load-bearing semantic detail from the original module.
@@ -826,7 +826,7 @@ fn main() -> Result<()> {
}
}
Cmd::Parse { path, output } => {
// Read .ailx, parse via the surface crate, emit canonical
// Read .ail, parse via the surface crate, emit canonical
// JSON. Symmetric to `render` (which goes the other way).
let src = std::fs::read_to_string(&path)
.with_context(|| format!("reading {}", path.display()))?;
+3 -3
View File
@@ -429,8 +429,8 @@ fn render_parse_round_trip_canonical() {
.expect("run ail render");
assert!(render.status.success(), "ail render failed");
let tmp = std::env::temp_dir().join(format!("ailang_render_e2e_{}.ailx", std::process::id()));
std::fs::write(&tmp, &render.stdout).expect("write rendered ailx");
let tmp = std::env::temp_dir().join(format!("ailang_render_e2e_{}.ail", std::process::id()));
std::fs::write(&tmp, &render.stdout).expect("write rendered ail");
let parsed = Command::new(ail_bin())
.args(["parse", tmp.to_str().unwrap()])
@@ -2153,7 +2153,7 @@ fn build_and_run_with_rc_stats(example: &str) -> (String, u64, u64, i64) {
/// not leak the LCons outer cells.
///
/// Background: 18f.2's tail-latency bench found that
/// `bench_latency_explicit.ailx` peaks at the same RSS as the
/// `bench_latency_explicit.ail` peaks at the same RSS as the
/// implicit-mode variant despite carrying mode annotations
/// throughout the hot path. Diagnosis: in
///
+7 -7
View File
@@ -62,7 +62,7 @@ fn roundtrip_one(fixture: &Path, tmpdir: &Path) -> Result<(), String> {
let bytes_orig = ailang_core::canonical::to_bytes(&original_module);
let h_orig = blake3::hash(&bytes_orig);
// Step B: `ail render <fixture>` → captured stdout = ailx text.
// Step B: `ail render <fixture>` → captured stdout = ail text.
let render_out = Command::new(ail_bin())
.args(["render", fixture.to_str().unwrap()])
.output()
@@ -75,20 +75,20 @@ fn roundtrip_one(fixture: &Path, tmpdir: &Path) -> Result<(), String> {
));
}
// Step C: write the rendered .ailx into the tempdir.
// Step C: write the rendered .ail into the tempdir.
let stem = fixture
.file_name()
.and_then(|n| n.to_str())
.and_then(|n| n.strip_suffix(".ail.json"))
.unwrap_or("fixture");
let tmp_ailx = tmpdir.join(format!("{stem}.round.ailx"));
std::fs::write(&tmp_ailx, &render_out.stdout)
.map_err(|e| format!("write tempfile {}: {e}", tmp_ailx.display()))?;
let tmp_ail = tmpdir.join(format!("{stem}.round.ail"));
std::fs::write(&tmp_ail, &render_out.stdout)
.map_err(|e| format!("write tempfile {}: {e}", tmp_ail.display()))?;
// Step D: `ail parse <tmp_ailx>` → captured stdout = canonical
// Step D: `ail parse <tmp_ail>` → captured stdout = canonical
// bytes + trailing newline.
let parse_out = Command::new(ail_bin())
.args(["parse", tmp_ailx.to_str().unwrap()])
.args(["parse", tmp_ail.to_str().unwrap()])
.output()
.map_err(|e| format!("spawn ail parse: {e}"))?;
if !parse_out.status.success() {
+7 -7
View File
@@ -2,7 +2,7 @@
Form-A is the canonical textual surface of AILang. It is the form that
LLMs generate when asked to produce or edit AILang code, and the form
that `ail parse <file>.ailx` reads. The inverse direction — printing
that `ail parse <file>.ail` reads. The inverse direction — printing
JSON-AST as Form-A — is `ail render <file>.ail.json`. Round-trip
through this pair is the gating contract: `parse(render(m)) == m`
for every well-formed module.
@@ -26,7 +26,7 @@ Lisp-style S-expression dress that:
- omits structural noise (no field tags; positions carry meaning)
- has a real parser with positional error messages
- round-trips through `ail render``ail parse` losslessly
- is the form every existing `examples/*.ailx` is written in
- is the form every existing `examples/*.ail` is written in
LLMs generate Form-A; the toolchain converts to JSON.
@@ -48,7 +48,7 @@ LLMs generate Form-A; the toolchain converts to JSON.
DEF*)
```
The module name MUST equal the file stem (`bench_list_sum.ailx`
The module name MUST equal the file stem (`bench_list_sum.ail`
`(module bench_list_sum ...)`).
## Imports
@@ -269,11 +269,11 @@ significantly.
## Few-shot corpus
These four modules are real `examples/*.ailx` content. Each one is
These four modules are real `examples/*.ail` content. Each one is
parseable and typechecks clean. Pattern-match against them when
generating new code.
### 1 — `hello.ailx`: minimal IO program
### 1 — `hello.ail`: minimal IO program
```
(module hello
@@ -283,7 +283,7 @@ generating new code.
(body (do io/print_str "Hello, AILang."))))
```
### 2 — `borrow_own_demo.ailx`: mode annotations on a recursive list
### 2 — `borrow_own_demo.ail`: mode annotations on a recursive list
```
(module borrow_own_demo
@@ -333,7 +333,7 @@ generating new code.
(do io/print_int (app sum_list xs)))))))
```
### 3 — `lit_pat.ailx`: literal patterns and nested ctor patterns
### 3 — `lit_pat.ail`: literal patterns and nested ctor patterns
```
(module lit_pat
+33 -33
View File
@@ -10,18 +10,18 @@
//! For every `examples/*.ail.json` fixture, load → `print` →
//! `parse` → canonical bytes; assert byte-equal to original.
//!
//! 2. `every_ailx_fixture_matches_its_json_counterpart`: hand-
//! authored ground-truth check. For every `examples/*.ailx`
//! 2. `every_ail_fixture_matches_its_json_counterpart`: hand-
//! authored ground-truth check. For every `examples/*.ail`
//! fixture, parse → canonical bytes; if a same-stem `.ail.json`
//! counterpart exists, assert canonical-byte equality against
//! it. Pins the `.ailx` corpus against semantic drift between
//! it. Pins the `.ail` corpus against semantic drift between
//! the two forms at the fixture level.
//!
//! 3. `parse_then_print_then_parse_is_idempotent_on_every_ailx_fixture`:
//! 3. `parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`:
//! Direction 2 of the Roundtrip Invariant. For every well-formed
//! `.ailx` text `t`, asserts `canonical_bytes(parse(t))` equals
//! `.ail` text `t`, asserts `canonical_bytes(parse(t))` equals
//! `canonical_bytes(parse(print(parse(t))))`. Robust against
//! future `.ailx` fixtures without a JSON counterpart.
//! future `.ail` fixtures without a JSON counterpart.
use std::path::{Path, PathBuf};
@@ -100,7 +100,7 @@ fn round_trip_one(path: &Path) -> Result<(), String> {
Ok(())
}
fn list_ailx_fixtures() -> Vec<PathBuf> {
fn list_ail_fixtures() -> Vec<PathBuf> {
let dir = examples_dir();
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display()))
@@ -109,7 +109,7 @@ fn list_ailx_fixtures() -> Vec<PathBuf> {
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| n.ends_with(".ailx"))
.map(|n| n.ends_with(".ail"))
.unwrap_or(false)
})
.collect();
@@ -118,11 +118,11 @@ fn list_ailx_fixtures() -> Vec<PathBuf> {
}
#[test]
fn every_ailx_fixture_matches_its_json_counterpart() {
let fixtures = list_ailx_fixtures();
fn every_ail_fixture_matches_its_json_counterpart() {
let fixtures = list_ail_fixtures();
assert!(
!fixtures.is_empty(),
"no .ailx fixtures found under {}",
"no .ail fixtures found under {}",
examples_dir().display()
);
@@ -130,32 +130,32 @@ fn every_ailx_fixture_matches_its_json_counterpart() {
let mut paired = 0usize;
let mut parse_only = 0usize;
for ailx_path in &fixtures {
let text = match std::fs::read_to_string(ailx_path) {
for ail_path in &fixtures {
let text = match std::fs::read_to_string(ail_path) {
Ok(t) => t,
Err(e) => {
failures.push(format!("{}: read failed: {e}", ailx_path.display()));
failures.push(format!("{}: read failed: {e}", ail_path.display()));
continue;
}
};
let parsed = match ailang_surface::parse(&text) {
Ok(m) => m,
Err(e) => {
failures.push(format!("{}: parse failed: {e}", ailx_path.display()));
failures.push(format!("{}: parse failed: {e}", ail_path.display()));
continue;
}
};
// Same-stem counterpart lookup. `<stem>.ailx` → `<stem>.ail.json`.
let stem = ailx_path
// Same-stem counterpart lookup. `<stem>.ail` → `<stem>.ail.json`.
let stem = ail_path
.file_name()
.and_then(|n| n.to_str())
.and_then(|n| n.strip_suffix(".ailx"))
.and_then(|n| n.strip_suffix(".ail"))
.unwrap_or("");
let json_path = ailx_path.with_file_name(format!("{stem}.ail.json"));
let json_path = ail_path.with_file_name(format!("{stem}.ail.json"));
if !json_path.exists() {
// Spec: parse success alone is sufficient for `.ailx` files
// Spec: parse success alone is sufficient for `.ail` files
// without a JSON counterpart. (Today: none expected.)
parse_only += 1;
continue;
@@ -166,7 +166,7 @@ fn every_ailx_fixture_matches_its_json_counterpart() {
Err(e) => {
failures.push(format!(
"{}: load_module({}) failed: {e}",
ailx_path.display(),
ail_path.display(),
json_path.display()
));
continue;
@@ -179,7 +179,7 @@ fn every_ailx_fixture_matches_its_json_counterpart() {
let s_round = String::from_utf8_lossy(&bytes_parsed).into_owned();
failures.push(format!(
"{} vs {}: canonical bytes differ.\noriginal: {s_orig}\nparsed: {s_round}",
ailx_path.display(),
ail_path.display(),
json_path.display()
));
continue;
@@ -189,7 +189,7 @@ fn every_ailx_fixture_matches_its_json_counterpart() {
if !failures.is_empty() {
panic!(
"{} of {} .ailx fixture(s) failed cross-check (paired ok: {}, parse-only: {}):\n{}",
"{} of {} .ail fixture(s) failed cross-check (paired ok: {}, parse-only: {}):\n{}",
failures.len(),
fixtures.len(),
paired,
@@ -198,27 +198,27 @@ fn every_ailx_fixture_matches_its_json_counterpart() {
);
}
eprintln!(
".ailx cross-check ok ({} paired, {} parse-only)",
".ail cross-check ok ({} paired, {} parse-only)",
paired, parse_only
);
}
/// Direction 2 of the Roundtrip Invariant (DESIGN.md §"Roundtrip
/// Invariant"): for every well-formed `.ailx` text `t`, the
/// Invariant"): for every well-formed `.ail` text `t`, the
/// composition `parse → print → parse` is idempotent on the AST.
///
/// For the 57 `.ailx` fixtures that have a JSON counterpart this
/// For the 57 `.ail` fixtures that have a JSON counterpart this
/// follows logically from `print_then_parse_round_trips_every_fixture`
/// + `every_ailx_fixture_matches_its_json_counterpart`. This test
/// + `every_ail_fixture_matches_its_json_counterpart`. This test
/// asserts the property directly so it stays robust for future
/// `.ailx` fixtures without a JSON counterpart, and so the spec's
/// `.ail` fixtures without a JSON counterpart, and so the spec's
/// Direction-2 claim has a dedicated enforcement point.
#[test]
fn parse_then_print_then_parse_is_idempotent_on_every_ailx_fixture() {
let fixtures = list_ailx_fixtures();
fn parse_then_print_then_parse_is_idempotent_on_every_ail_fixture() {
let fixtures = list_ail_fixtures();
assert!(
!fixtures.is_empty(),
"no .ailx fixtures found under {}",
"no .ail fixtures found under {}",
examples_dir().display()
);
@@ -266,12 +266,12 @@ fn parse_then_print_then_parse_is_idempotent_on_every_ailx_fixture() {
if !failures.is_empty() {
panic!(
"{} of {} .ailx fixture(s) failed idempotency check (passed: {}):\n{}",
"{} of {} .ail fixture(s) failed idempotency check (passed: {}):\n{}",
failures.len(),
fixtures.len(),
passed,
failures.join("\n\n")
);
}
eprintln!("parse→print→parse idempotency ok for {passed} .ailx fixtures");
eprintln!("parse→print→parse idempotency ok for {passed} .ail fixtures");
}
+25 -25
View File
@@ -175,7 +175,7 @@ Trade-off: no inline optimisations through the LLVM API. We rely on
Form (A) is implemented as
the `ailang-surface` crate (parser + printer). Form-A is
gated against drift by `ailang-surface/tests/round_trip.rs`, which
parses every `.ailx` fixture, prints it back, re-parses, and demands
parses every `.ail` fixture, prints it back, re-parses, and demands
canonical-byte equality. `ail render` and both branches
of `ail describe` were rewired to use `ailang_surface::print`, making
form (A) the **sole** text projection of a module — the legacy
@@ -247,7 +247,7 @@ the AST and remain projection-agnostic.
semantic indentation, maximal-munch lexing, context-sensitive
reductions.
2. **AST-isomorphic.** Every surface form maps to exactly one AST
shape. The full bijection between `.ail.json` and `.ailx` (both
shape. The full bijection between `.ail.json` and `.ail` (both
directions, BLAKE3-stable hashing, Float-bits-hex encoding,
workspace-CI enforcement points) is anchored as the top-level
§"Roundtrip Invariant" — this constraint records that Decision
@@ -437,11 +437,11 @@ on the shelf for a future iter only if both fail.
continue to consume `ailang-core::ast::Module` values regardless
of which projection produced them.
- Round-trip test: for every `examples/*.ail.json`, parse the
corresponding hand-written `*.ailx`, canonicalise, and assert
corresponding hand-written `*.ail`, canonicalise, and assert
hash-equivalence to the original. Hash equivalence is the truth
check; the surface ships only if every fixture round-trips
identically.
- CLI: `ail parse <file.ailx> -o <file.ail.json>`. Symmetric to
- CLI: `ail parse <file.ail> -o <file.ail.json>`. Symmetric to
existing `ail render`. **`.ail.json` remains a first-class input**
to every existing subcommand; the parser is a producer, not a
gatekeeper.
@@ -497,7 +497,7 @@ Neither change extends the grammar's rule budget meaningfully:
the 30-production ceiling of constraint 1 is intact (the parser
implements ~28 named productions). All 17 `examples/*.ail.json`
fixtures round-trip identically through `print → parse → canonical
JSON`; the three hand-written `.ailx` exhibits parse to canonical
JSON`; the three hand-written `.ail` exhibits parse to canonical
JSON identical to their corresponding `.ail.json` files.
3. **Tail-call surface.** Decision 8 ships two new
@@ -580,12 +580,12 @@ foreign LLM enough to produce valid output. The current prompt revises this:
- The LLM emits **Form-A** (the canonical authoring surface), not
JSON. JSON-AST stays the only hashable artefact, but it is not
a writing surface. The user runs `ail parse foo.new.ailx`
a writing surface. The user runs `ail parse foo.new.ail`
before `ail check` to produce the canonical JSON.
- `crates/ailang-core/specs/form_a.md` is the complete LLM-targeted
Form-A specification — grammar, every term / pattern / type /
def keyword, schema invariants, pitfall catalogue, four
few-shot modules drawn from `examples/*.ailx`. It is exported
few-shot modules drawn from `examples/*.ail`. It is exported
as `ailang_core::FORM_A_SPEC` and embedded verbatim in every
`merge-prose` prompt.
- `crates/ailang-core/tests/spec_drift.rs` walks every variant of
@@ -615,7 +615,7 @@ so JSON-cohort feedback carries the full anyhow cause chain):
- `meta-llama/CodeLlama-13b-Instruct-hf` —
`experiments/2026-05-12-cross-model-authoring/runs/2026-05-12-9197fd/`
| metric | Qwen / JSON | Qwen / AILX | CodeLlama / JSON | CodeLlama / AILX |
| metric | Qwen / JSON | Qwen / AIL | CodeLlama / JSON | CodeLlama / AIL |
|---|---|---|---|---|
| reached green | 1/4 | 1/4 | 0/4 | 2/4 |
| first-attempt green | 1/4 | 1/4 | 0/4 | 2/4 |
@@ -625,16 +625,16 @@ so JSON-cohort feedback carries the full anyhow cause chain):
| top error class | check (×3) | parse (×3) | check (×2) | parse (×2) |
Both subjects show the same direction on every metric the table
tracks. AILX is cheaper than JSON on prompt tokens (Qwen 61%,
tracks. AIL is cheaper than JSON on prompt tokens (Qwen 61%,
CodeLlama 80% of the JSON cohort's spend) and cheaper on completion
tokens (Qwen 23%, CodeLlama 82%). AILX reached-green is greater than
tokens (Qwen 23%, CodeLlama 82%). AIL reached-green is greater than
or equal to JSON reached-green for each subject (Qwen ties at 1/4;
CodeLlama strictly dominates, 2/4 vs 0/4). The failure-class
symmetry observed on the original Qwen baseline holds for both
subjects: JSON cohorts fail at the typecheck stage, AILX cohorts at
subjects: JSON cohorts fail at the typecheck stage, AIL cohorts at
the parse stage — each form's front-of-pipeline check. Three of the
four first-attempt-green cells observed across the two subjects are
AILX (Qwen-AILX t3, CodeLlama-AILX t1, CodeLlama-AILX t3); the
AIL (Qwen-AIL t3, CodeLlama-AIL t1, CodeLlama-AIL t3); the
fourth is Qwen-JSON t3, the same task the original baseline already
flagged as the JSON-form's only first-attempt success. Two CodeLlama
JSON-cohort cells (`t2_length`, `t4_count_zeros`) terminated as
@@ -642,7 +642,7 @@ JSON-cohort cells (`t2_length`, `t4_count_zeros`) terminated as
IONOS-side terminal error, not a model-output failure; CodeLlama
JSON-cohort numbers therefore average over 2 informative cells, not
4. The Qwen re-run also shifted by one cell against the cma.3
baseline (AILX 2/4 → 1/4, JSON 1/4 → 1/4) despite identical
baseline (AIL 2/4 → 1/4, JSON 1/4 → 1/4) despite identical
temperature=0 inputs; this is IONOS-side state-of-day noise on a
single deterministic re-run, not an effect of the ms.1
feedback-formatting fix (which only changes JSON-cohort prompt
@@ -650,10 +650,10 @@ content).
**Scope of this addendum:** two subjects, n=1 each, deterministic.
This is a second data point pointing in the same direction as the
first, not a verdict. The universal claim of this Decision (".ailx
first, not a verdict. The universal claim of this Decision (".ail
is the AI authoring projection") would need ≥3 subjects with
statistical robustness to ratify; both current points point the
same way (AILX cohort cheaper and at-least-as-green) but neither
same way (AIL cohort cheaper and at-least-as-green) but neither
the sample size nor the cross-call noise observed in the Qwen
re-run is enough to close out the Decision.
@@ -1813,7 +1813,7 @@ would invoke literal-defaulting which axis-7 already excluded.
## Roundtrip Invariant
Every well-formed AILang module has both a canonical `.ail.json`
representation and a textual `.ailx` representation, and the two
representation and a textual `.ail` representation, and the two
are exact projections of the same AST. Concretely, both directions
of the bijection hold:
@@ -1822,7 +1822,7 @@ of the bijection hold:
`canonical::to_bytes(load_module(J))`. The textual surface is
the inverse of the loader composed with the printer; no
information is lost across the round-trip.
2. **text → JSON → text.** For every well-formed `.ailx` text `t`,
2. **text → JSON → text.** For every well-formed `.ail` text `t`,
`parse(t)` is a complete AST, and re-printing `print(parse(t))`
produces a text that re-parses to the same AST (modulo
formatting). The parser is total over the well-formed surface
@@ -1854,13 +1854,13 @@ inherit the gate automatically):
- `crates/ailang-surface/tests/round_trip.rs::print_then_parse_round_trips_every_fixture`
— for every `.ail.json`, `print` then `parse` produces canonical-
byte-equal output. Direction 1 above.
- `crates/ailang-surface/tests/round_trip.rs::parse_then_print_then_parse_is_idempotent_on_every_ailx_fixture`
— for every `.ailx` text `t`, `parse(t)` and `parse(print(parse(t)))`
- `crates/ailang-surface/tests/round_trip.rs::parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`
— for every `.ail` text `t`, `parse(t)` and `parse(print(parse(t)))`
produce canonical-byte-equal AST. Direction 2 above.
- `crates/ailang-surface/tests/round_trip.rs::every_ailx_fixture_matches_its_json_counterpart`
— for every `.ailx` with a same-stem `.ail.json` counterpart,
- `crates/ailang-surface/tests/round_trip.rs::every_ail_fixture_matches_its_json_counterpart`
— for every `.ail` with a same-stem `.ail.json` counterpart,
`parse` of the text yields canonical bytes equal to the JSON
counterpart. Pins the hand-authored `.ailx` corpus against
counterpart. Pins the hand-authored `.ail` corpus against
drift between the two forms at the fixture level.
- `crates/ailang-core/tests/schema_coverage.rs::every_ast_variant_is_observed_in_the_fixture_corpus`
— every variant of `Def`, `Term`, `Pattern`, `Literal`, `Type`,
@@ -1882,7 +1882,7 @@ never by relaxing the test.
### Why this is anchored at top level
The invariant is a property of the language identity, not of any
one surface-design Decision. Decision 6 introduces the `.ailx`
one surface-design Decision. Decision 6 introduces the `.ail`
surface and lists round-trip-as-property as one of its
constraints, but the property is load-bearing for every
downstream concern that treats the two forms as exchangeable:
@@ -2186,7 +2186,7 @@ ail check <module.ail.json> — loads, validates, typechecks
ail manifest <module.ail.json> — table: name :: type !effects [hash]
ail describe <module> <name> — detail of a definition (form-A body)
ail render <module.ail.json> — JSON-AST → form-A text (exact inverse of `parse`)
ail parse <module.ailx> — form-A text → canonical JSON-AST
ail parse <module.ail> — form-A text → canonical JSON-AST
ail prose <module.ail.json> — JSON-AST → form-B (lossy human prose, no parser)
ail merge-prose <m.ail.json> <m.prose.txt>
— compose the LLM-mediator prompt for the prose round-trip
@@ -2403,7 +2403,7 @@ What **is** supported (and used as the smoke test for the pipeline):
`(term-ctor std_pair.Pair MkPair x y)` and `(pat-ctor MkPair x y)`
inside that scrutinee. Std-library demos (`examples/std_*_demo.ail.json`)
exercise this end-to-end.
- **AI-authoring text surface, form (A)** (Decision 6). The `ailang-surface` crate parses `.ailx` form-A
- **AI-authoring text surface, form (A)** (Decision 6). The `ailang-surface` crate parses `.ail` form-A
text into a canonical `ailang-core::ast::Module` and prints any module
back as form-A text. `ail render` and `ail describe` use it as the
sole text projection; `ail parse` is the inverse direction. Round-trip
+5 -5
View File
@@ -19,14 +19,14 @@ modes to watch for.
## What the LLM produces
Iter 20f revised a load-bearing piece of this cycle: the LLM emits
**Form-A** (an `.ailx` document — the canonical authoring surface
**Form-A** (an `.ail` document — the canonical authoring surface
fixed by Decision 6), not JSON-AST. JSON-AST is the canonical
hashable artefact, but it is not a writing surface — every type
reference wraps in `{"k": "con", ...}`, every term in
`{"t": "...", ...}`, and a single missing `param_modes` entry is a
schema error rather than a parse error with a position.
Form-A is the form `examples/*.ailx` are written in, the form
Form-A is the form `examples/*.ail` are written in, the form
`ail render` produces, and the form `ail parse` consumes. The full
LLM-targeted specification is shipped inside the binary as
`ailang_core::FORM_A_SPEC` (sourced from
@@ -39,8 +39,8 @@ LLM-targeted specification is shipped inside the binary as
1. ail prose foo.ail.json > foo.prose.txt
2. $EDITOR foo.prose.txt # human edits freely
3. ail merge-prose foo.ail.json foo.prose.txt > prompt.txt
4. cat prompt.txt | <your-llm-cli> > foo.new.ailx
5. ail parse foo.new.ailx > foo.new.ail.json
4. cat prompt.txt | <your-llm-cli> > foo.new.ail
5. ail parse foo.new.ail > foo.new.ail.json
6. ail check foo.new.ail.json
7. mv foo.new.ail.json foo.ail.json # if check is clean
```
@@ -74,7 +74,7 @@ CLI helper.
You are integrating prose edits back into an AILang module.
ROLE
Your job is to produce an updated AILang module in Form-A (an .ailx
Your job is to produce an updated AILang module in Form-A (an .ail
file) that reflects the human's prose edits while preserving the
load-bearing semantic detail from the original module.
+107
View File
@@ -0,0 +1,107 @@
# iter ext-rename — `.ailx` extension renamed to `.ail` across the live toolchain
**Date:** 2026-05-12
**Started from:** 17b370b (post-audit-ms close, working tree clean)
**Status:** DONE
**Tasks completed:** 1 of 1
## Summary
The surface-form file extension changes from `.ailx` to `.ail`.
AILang's authoring surface now uses the same `.ail` stem as its
canonical JSON form (`.ail.json`), giving the language a single
coherent extension family: `.ail` is the LLM-authored Form A;
`.ail.json` is the canonical JSON-AST Form B that
`ail check` / `build` / `run` actually loads today.
The earlier `.ailx` extension carried an unmotivated `x`. The
cross-model authoring-form test (cma + ms milestones, closed
earlier today) treated `.ailx` purely as a cohort identifier;
once the empirical case for keeping a separate authoring surface
had been ratified (DESIGN.md §Decision-6 addendum), the awkward
extension was the last asymmetry between Form A and Form B.
User-initiated rename.
## Scope
In scope (touched):
- 61 example files `examples/**/*.ailx → .ail` (57 top-level +
4 `examples/fieldtest/`) renamed via `git mv`.
- `experiments/.../rendered/ailx.md → ail.md` rename.
- All live toolchain prose and code: `crates/ail/`,
`crates/ailang-surface/`, `crates/ailang-core/specs/form_a.md`,
`docs/DESIGN.md`, `docs/roadmap.md`, `docs/PROSE_ROUNDTRIP.md`,
`bench/reference/*.c`, all of `skills/`, the Cross-Model
Authoring experiment crates under
`experiments/.../{render,harness,master,rendered,README.md}`.
- Cohort identifier `ailx → ail` in the experiment crate (Rust
`Cohort::Ailx → Cohort::Ail`, `Form::Ailx → Form::Ail`,
per-cohort path component, `{form-only: ailx}` marker,
```ailx codefence language tag).
Out of scope (untouched, by design):
- `docs/journal-archive.md` (content-frozen per CLAUDE.md).
- All `docs/journals/`, `docs/specs/`, `docs/plans/`,
`bench/orchestrator-stats/` — historical records describing
states as they were.
- `experiments/.../runs/` — frozen LLM-output artefacts; the
models in those runs actually saw `.ailx`, and renaming the
artefacts would falsify the experimental record.
The exclusion preserves the journals as honest history. Anyone
chasing why a 2026-05-11 fieldtest names `.ailx` files will find
that the renaming happened today and that the names were correct
at the time of writing.
## Why boss-direct edit (no planner / implement dispatch)
Same rationale as iter rt.2: the user fully specified the
transformation (rename `.ailx → .ail`, rename cohort
`ailx → ail`, historical records untouched), down to the three
open questions that were resolved up-front via AskUserQuestion
(cohort name; treatment of frozen runs; treatment of historical
docs). The execution was two `git mv` passes plus one
heavily-scoped `sed` over an explicit 41-file whitelist (plus
two follow-up files surfaced by the negative-grep). No design
judgement deferred to execution time.
## Files touched (working-tree counts)
- 61 renames under `examples/`.
- 1 rename `experiments/.../rendered/ailx.md → ail.md`.
- 35 content-edited files: the 41-file SCOPE whitelist minus
the experiment-crate files that had no `ailx` content, plus
two follow-up files surfaced by the negative-grep
(`examples/bench_list_sum_explicit.ail`'s in-source comment;
`experiments/.../rendered/json.md` for three `AILX` acronyms
in prose).
## Verification
- `cargo build --workspace` clean.
- `cargo test --workspace` green; the renamed identifiers
`every_ail_fixture_matches_its_json_counterpart` and
`parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`
in `crates/ailang-surface/tests/round_trip.rs` were run
explicitly and pass.
- Experiment crate `cargo test` (out-of-workspace) green.
- `bench/check.py`, `bench/compile_check.py`, `bench/cross_lang.py`:
0 regressed across all three; 6 improved-beyond-tolerance on
`latency.explicit_at_rc` (same cluster the previous two audits
today already observed; decoupled from this rename — baseline
left pristine).
- Negative grep: `git grep -nIi 'ailx|Ailx|AILX'` outside the
out-of-scope paths returns no matches.
## Roadmap implications
The `.ail` extension is now canonical authoring surface but the
CLI does not yet accept it — `ail check foo.ail` still produces
the misleading JSON-parse error. The existing P2 todo
"`ail check`/`build`/`run` accept `.ail` extension" (roadmap.md
line 185) is the natural next iteration: it closes the loop
opened by today's rename and removes the largest first-command
friction for any LLM-author writing in Form A. Promoting that
todo to the immediate next dispatch.
+1
View File
@@ -28,3 +28,4 @@
- 2026-05-12 — iter ms.1: pipeline anyhow-chain preservation — `{e}``{e:#}` at `pipeline.rs:118` restores `serde_json` parse-error leaf in JSON-cohort feedback; pinning unit test added (14/14 green) → 2026-05-12-iter-ms.1.md
- 2026-05-12 — iter ms.2: Qwen3-Coder-Next retroactive re-run + first CodeLlama-13b-Instruct run via IONOS; DESIGN.md §Decision-6 addendum extended from 2-col single-subject to 4-col two-subject; both subjects agree on direction (AILX cheaper + ≥ green); roadmap P3 multi-subject entry removed → 2026-05-12-iter-ms.2.md
- 2026-05-12 — audit-ms: milestone close (Multi-subject Authoring-Form Test — CodeLlama Replication) — architect clean, bench all-green (same 5-metric latency.explicit_at_rc improvement cluster as audit-cma earlier today, baseline left pristine for second consecutive audit) → 2026-05-12-audit-ms.md
- 2026-05-12 — iter ext-rename: `.ailx``.ail` extension rename across the live toolchain (61 example renames + 35 content edits + experiment-crate cohort rename `ailx → ail`); historical docs (journals, archive, specs, plans, frozen experiment runs) deliberately untouched; cargo+bench all-green; opens follow-up `.ail`-CLI-acceptance iter → 2026-05-12-iter-ext-rename.md
+9 -9
View File
@@ -171,7 +171,7 @@ context. Pick the next milestone from P1.)_
type name). In the known-owner branch, list the owner's available
type defs as candidates the way `bare-cross-module-type-ref`
lists candidates from imports.
- context: fieldtest 2026-05-11 — `examples/ct_3*.ailx` exhibits both branches with identical-shape diagnostics.
- context: fieldtest 2026-05-11 — `examples/ct_3*.ail` exhibits both branches with identical-shape diagnostics.
- [ ] **\[todo\]** Workspace search beyond entry-module's directory —
`load_workspace` only finds sibling `.ail.json` files in the same
directory as the entry module, so any consumer of prelude/std in a
@@ -182,12 +182,12 @@ context. Pick the next milestone from P1.)_
- context: fieldtest 2026-05-11 — fieldtest fixtures could not be
placed under `examples/fieldtest/` because of this; predates the
canonical-type-names milestone but surfaces every time.
- [ ] **\[todo\]** `ail check`/`build`/`run` accept `.ailx` extension —
today `ail check foo.ailx` produces a misleading JSON-parse error
- [ ] **\[todo\]** `ail check`/`build`/`run` accept `.ail` extension —
today `ail check foo.ail` produces a misleading JSON-parse error
(`json: expected value at line 1 column 1`) because the loader only
accepts `.ail.json`. Either teach the CLI subcommands to auto-parse
`.ailx` internally, or detect the extension and emit a
"did you mean `ail parse foo.ailx`?" hint. The LLM-author's natural
`.ail` internally, or detect the extension and emit a
"did you mean `ail parse foo.ail`?" hint. The LLM-author's natural
first command should not be the one that produces a misleading
diagnostic.
- context: fieldtest 2026-05-11 — orthogonal to canonical-type-names
@@ -205,13 +205,13 @@ context. Pick the next milestone from P1.)_
## P3 — Ideas
- [ ] **\[todo\]** `compare_primitives_smoke.ailx` counterpart —
- [ ] **\[todo\]** `compare_primitives_smoke.ail` counterpart —
the canonical happy-path exhibit for canonical-type-names ships
only as JSON. Per Decision 6, Surface is the LLM-author surface;
the milestone should have a `.ailx` counterpart. Two paths: author
`examples/compare_primitives_smoke.ailx` to round-trip into the
the milestone should have a `.ail` counterpart. Two paths: author
`examples/compare_primitives_smoke.ail` to round-trip into the
existing JSON, OR retire the JSON fixture in favour of
`examples/ct_1_ordering_signum.{ailx,ail.json}` as the new canonical
`examples/ct_1_ordering_signum.{ail,ail.json}` as the new canonical
happy-path exhibit.
- context: fieldtest 2026-05-11 (spec_gap).
- [ ] **\[todo\]** Codegen `lookup_ctor_in_pattern` type-anchoring —
@@ -1,6 +1,6 @@
; Bench fixture: explicit-mode pair of bench_list_sum.
;
; Same algorithm and same sizes as bench_list_sum.ailx (build a list of
; Same algorithm and same sizes as bench_list_sum.ail (build a list of
; 0..N-1, sum it, print) but with full `(borrow)` / `(own)` annotations
; on every fn-param signature in the hot path. Under --alloc=rc the
; codegen now emits `inc`/`dec` instructions: each cell allocated by
@@ -1,6 +1,6 @@
# Cross-model authoring-form test
Empirical measurement of whether `.ail.json` or `.ailx` is the form a
Empirical measurement of whether `.ail.json` or `.ail` is the form a
foreign LLM author reaches for and succeeds with. Single subject for
v1: Qwen3-Coder-Next via IONOS. Two blind cohorts; same four tasks.
@@ -16,7 +16,7 @@ Parent spec: `docs/specs/2026-05-12-cross-model-authoring-form-test.md`.
harness. **Authored in cma.2**, not cma.1.
- `render/` — standalone Cargo crate, outside the root workspace,
builds the renderer binary.
- `rendered/json.md`, `rendered/ailx.md` — projected mini-specs,
- `rendered/json.md`, `rendered/ail.md` — projected mini-specs,
checked into the repo for review.
- `harness/`**Authored in cma.2**.
- `runs/<date>-<hash>/` — populated by `harness` during a live run.
@@ -36,7 +36,7 @@ cargo test --manifest-path experiments/2026-05-12-cross-model-authoring/render/C
```
Three integration tests: `example_roundtrip` (each example loads,
prints to AILX, reparses to the same canonical bytes), `spec_completeness`
prints to AIL, reparses to the same canonical bytes), `spec_completeness`
(every AST variant in `ailang_core::ast` is exercised by at least
one example), `token_balance` (form-only blocks balanced within ±5%
across the two rendered files).
@@ -42,8 +42,8 @@ fn main() -> Result<()> {
let rendered_json = std::fs::read_to_string(args.rendered.join("json.md"))
.with_context(|| format!("reading {}", args.rendered.join("json.md").display()))?;
let rendered_ailx = std::fs::read_to_string(args.rendered.join("ailx.md"))
.with_context(|| format!("reading {}", args.rendered.join("ailx.md").display()))?;
let rendered_ail = std::fs::read_to_string(args.rendered.join("ail.md"))
.with_context(|| format!("reading {}", args.rendered.join("ail.md").display()))?;
let tasks = tasks::load_all(&args.tasks)?;
if tasks.is_empty() { bail!("no tasks found in {}", args.tasks.display()); }
@@ -52,8 +52,8 @@ fn main() -> Result<()> {
let mut run_status = "ok";
let mut completed: std::collections::BTreeSet<(&'static str, String)> = Default::default();
'outer: for cohort in [Cohort::Json, Cohort::Ailx] {
let system_prompt = match cohort { Cohort::Json => &rendered_json, Cohort::Ailx => &rendered_ailx };
'outer: for cohort in [Cohort::Json, Cohort::Ail] {
let system_prompt = match cohort { Cohort::Json => &rendered_json, Cohort::Ail => &rendered_ail };
for t in &tasks {
let (row, consumed) = run_one(
&backend, &args, cohort, system_prompt, t,
@@ -75,7 +75,7 @@ fn main() -> Result<()> {
// walking every (cohort, task) pair. The pairs we never reached
// still need a row, with final_status = budget_abort.
if run_status == "budget_exceeded" {
for cohort in [Cohort::Json, Cohort::Ailx] {
for cohort in [Cohort::Json, Cohort::Ail] {
for t in &tasks {
if !completed.contains(&(cohort.as_str(), t.id.clone())) {
rows.push(ScoreRow {
@@ -7,7 +7,7 @@
//! "t1_add_three": { "1": { "content": "...", "usage": {...} } },
//! "t2_length": { "1": {...}, "2": {...} }
//! },
//! "ailx": { ... }
//! "ail": { ... }
//! }
//! ```
//! Where each `"<turn>"` entry has `"content"` (the program the
@@ -12,20 +12,20 @@ use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cohort {
Json,
Ailx,
Ail,
}
impl Cohort {
pub fn extension(self) -> &'static str {
match self {
Cohort::Json => "ail.json",
Cohort::Ailx => "ailx",
Cohort::Ail => "ail",
}
}
pub fn as_str(self) -> &'static str {
match self {
Cohort::Json => "json",
Cohort::Ailx => "ailx",
Cohort::Ail => "ail",
}
}
}
@@ -79,8 +79,8 @@ pub fn run_pipeline(
let prog_path = workdir.join(format!("prog.{}", cohort.extension()));
std::fs::write(&prog_path, program).with_context(|| format!("writing {}", prog_path.display()))?;
// For AILX cohort, parse to JSON first.
let json_path_initial = if matches!(cohort, Cohort::Ailx) {
// For AIL cohort, parse to JSON first.
let json_path_initial = if matches!(cohort, Cohort::Ail) {
let out = workdir.join("prog.ail.json");
let parse_out = Command::new(ail_bin())
.arg("parse").arg(&prog_path)
@@ -68,7 +68,7 @@ pub fn write_summary_md(rows: &[ScoreRow], path: &Path) -> Result<()> {
let mut f = std::fs::File::create(path)
.with_context(|| format!("creating {}", path.display()))?;
writeln!(f, "# Cross-model authoring-form test — run summary\n")?;
for cohort in ["json", "ailx"] {
for cohort in ["json", "ail"] {
writeln!(f, "## Cohort: {cohort}\n")?;
let cohort_rows: Vec<&ScoreRow> = rows.iter().filter(|r| r.cohort == cohort).collect();
if cohort_rows.is_empty() {
@@ -4,7 +4,7 @@
//! The intent (parent spec §strip_locations) is **symmetric
//! degradation**: both cohorts lose the localisation information
//! their compiler natively produces. The JSON cohort would
//! otherwise get JSON-pointer fragments; the AILX cohort would get
//! otherwise get JSON-pointer fragments; the AIL cohort would get
//! `at byte N` offsets. Neither survives this pass.
use regex::Regex;
@@ -49,7 +49,7 @@
}
}
},
"ailx": {
"ail": {
"t1_add_three": {
"1": {
"content": "(module garbage",
@@ -1,7 +1,7 @@
//! End-to-end mock-mode test (parent spec §Testing strategy).
//! Runs the harness binary against a canned response file that
//! makes (json, t3_main_prints) green on turn 2 and
//! (ailx, t1_add_three) run to the turn limit. Asserts the
//! (ail, t1_add_three) run to the turn limit. Asserts the
//! scores.csv is well-formed and the right per-(cohort,task)
//! artefacts land on disk.
@@ -35,10 +35,10 @@ fn mock_full_run_produces_scores_and_artefacts() {
assert!(scores.starts_with("cohort,task_id,first_attempt_green,"));
assert_eq!(scores.lines().count(), 1 + 8, "header + 8 rows expected");
assert!(scores.contains("json,t3_main_prints,false,2"));
assert!(scores.contains("ailx,t1_add_three,false,INF"));
assert!(scores.contains("ail,t1_add_three,false,INF"));
// Spot-check one per-cohort artefact tree.
let pc = run_dir.join("per_cohort").join("ailx").join("t1_add_three");
assert!(pc.join("turn_1_program.ailx").exists());
assert!(pc.join("turn_5_program.ailx").exists());
let pc = run_dir.join("per_cohort").join("ail").join("t1_add_three");
assert!(pc.join("turn_1_program.ail").exists());
assert!(pc.join("turn_5_program.ail").exists());
}
@@ -46,8 +46,8 @@ pattern object carries a `p` discriminator. Every literal object
carries a `kind` discriminator.
{/form-only}
{form-only: ailx}
The AILX surface is parenthesised tagged-head Lisp-style notation.
{form-only: ail}
The AIL surface is parenthesised tagged-head Lisp-style notation.
Every form begins with `(` followed by a tag identifier; positional
arguments follow; the form closes with `)`. There are no infix
operators, no operator precedence, no implicit conversions.
@@ -62,9 +62,9 @@ digit. String literals are double-quoted with `\"`, `\\`, `\n`, `\t`
escapes; integer literals are signed decimal; the `(unit)` keyword
denotes the unit value.
The AILX form is the printed dual of the JSON form: `ail parse
file.ailx` produces the same canonical bytes as the JSON form would,
and `ail render file.ail.json` produces the AILX text. Round-trip
The AIL form is the printed dual of the JSON form: `ail parse
file.ail` produces the same canonical bytes as the JSON form would,
and `ail render file.ail.json` produces the AIL text. Round-trip
through this pair is gated by an integration test.
{/form-only}
@@ -110,7 +110,7 @@ Type::Forall: `{"k": "forall", "vars": [...], "body": ...}` with an
optional `constraints` array for class constraints (see section 10).
{/form-only}
{form-only: ailx}
{form-only: ail}
Type::Con: `Int`, `Bool`, `Str`, `Float`, `Unit`, or a user type
name. For parameterised types: `(con List Int)` reads "List of Int";
nested: `(con Map (con Str) (con Int))`.
@@ -168,7 +168,7 @@ A fn that consumes a list and returns a new list:
`"param_modes": ["own"]`, `"ret_mode": "own"`.
{/form-only}
{form-only: ailx}
{form-only: ail}
Modes are surface wrappers around the type in parameter and return
position. Inside a `(fn-type ...)`:
@@ -235,7 +235,7 @@ use snake_case. This is historical and the canonical bytes preserve
the difference.
{/form-only}
{form-only: ailx}
{form-only: ail}
Function definition: `(fn NAME (doc STRING)? (type TYPE) (params
NAME*) (body TERM))`. The `doc` clause is optional but recommended.
@@ -295,7 +295,7 @@ constructor's declared `fields` in order; nullary constructors
carry `"args": []`.
{/form-only}
{form-only: ailx}
{form-only: ail}
Type definition: `(data NAME (vars TYVAR*)? (doc STRING)? (ctor
CTOR-NAME ARG-TYPE*)*)`. The `vars` clause is optional; nullary
constructors have an empty argument list.
@@ -349,7 +349,7 @@ Pattern variables are linear: each name appears at most once in a
single pattern. Repeating a name is a typecheck error.
{/form-only}
{form-only: ailx}
{form-only: ail}
Match expression: `(match SCRUT (case PAT BODY) (case PAT BODY)
...)`. Arms are introduced by `(case ...)`; the order matters (top-
to-bottom).
@@ -409,7 +409,7 @@ The value of the `seq` is the value of `rhs`; `lhs`'s value is
discarded but its effects count toward the enclosing fn's row.
{/form-only}
{form-only: ailx}
{form-only: ail}
Effect row on a fn type: `(effects E1 E2 ...)` inside the
`(fn-type ...)`. Empty row: omit the `(effects)` clause or write
`(effects)`. Common effectful row: `(effects IO)`. Order is sorted
@@ -435,10 +435,10 @@ There are five literal shapes. An integer literal is a signed
literal is a UTF-8 sequence in double quotes; AILang restricts the
authored byte set to ASCII printable (Decision 6 Constraint 3) — non-
ASCII bytes are an authoring error. A unit literal is the value of
type Unit, written `(unit)` in AILX and `{"kind": "unit"}` in JSON.
type Unit, written `(unit)` in AIL and `{"kind": "unit"}` in JSON.
A Float literal is an IEEE-754 binary64 value. When you author
Floats, write the decimal form (`3.14`) — the AILX parser converts
Floats, write the decimal form (`3.14`) — the AIL parser converts
to the 64-bit bit pattern and emits the canonical 16-character
lowercase hex string into the JSON. You never author the hex
encoding by hand; the toolchain owns that representation. NaN and
@@ -464,11 +464,11 @@ Literal variants and their JSON shapes:
serialisation drift across `serde_json` versions.
Float bit patterns are computed by the toolchain on `ail parse`,
not by the author. When you write `3.14` in AILX, the parser emits
not by the author. When you write `3.14` in AIL, the parser emits
`"bits": "40091eb851eb851f"` in the JSON form.
{/form-only}
{form-only: ailx}
{form-only: ail}
Literal surface forms:
- Int: bare signed decimal, e.g. `42`, `-7`, `0`.
@@ -533,7 +533,7 @@ Constraint on a polymorphic fn: inside `Type::Forall`, the
constraints; omitted from canonical bytes when empty.
{/form-only}
{form-only: ailx}
{form-only: ail}
Class declaration: `(class NAME (param TYVAR) (method NAME (type
METHOD-TYPE) (default BODY)?)*)`. The `default` clause is
optional; methods without a default must be supplied by every
@@ -2,8 +2,8 @@
//!
//! - JSON form: read the on-disk canonical bytes verbatim, emit a
//! ```json``` fenced block.
//! - AILX form: load the example via ailang_core::load_module, print
//! via ailang_surface::print, emit a ```ailx``` fenced block.
//! - AIL form: load the example via ailang_core::load_module, print
//! via ailang_surface::print, emit a ```ail``` fenced block.
use anyhow::{anyhow, Context, Result};
use std::path::Path;
@@ -11,7 +11,7 @@ use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Form {
Json,
Ailx,
Ail,
}
pub fn render_example(examples_dir: &Path, id: &str, form: Form) -> Result<String> {
@@ -28,11 +28,11 @@ pub fn render_example(examples_dir: &Path, id: &str, form: Form) -> Result<Strin
.with_context(|| format!("reading {}", path.display()))?;
Ok(format!("```json\n{}\n```\n", bytes.trim_end()))
}
Form::Ailx => {
Form::Ail => {
let module = ailang_core::load_module(&path)
.with_context(|| format!("load_module on {}", path.display()))?;
let text = ailang_surface::print(&module);
Ok(format!("```ailx\n{}\n```\n", text.trim_end()))
Ok(format!("```ail\n{}\n```\n", text.trim_end()))
}
}
}
@@ -1,4 +1,4 @@
//! xmodel-render — projects master/spec.md into rendered/json.md and rendered/ailx.md.
//! xmodel-render — projects master/spec.md into rendered/json.md and rendered/ail.md.
use anyhow::{anyhow, Context, Result};
use std::path::{Path, PathBuf};
@@ -15,17 +15,17 @@ fn main() -> Result<()> {
let segments = splitter::split(&spec_text);
let json_out = render_for(&segments, &examples_dir, examples::Form::Json, splitter::Form::Json)?;
let ailx_out = render_for(&segments, &examples_dir, examples::Form::Ailx, splitter::Form::Ailx)?;
let ail_out = render_for(&segments, &examples_dir, examples::Form::Ail, splitter::Form::Ail)?;
std::fs::create_dir_all(&rendered_dir)
.with_context(|| format!("creating {}", rendered_dir.display()))?;
let json_path = rendered_dir.join("json.md");
let ailx_path = rendered_dir.join("ailx.md");
let ail_path = rendered_dir.join("ail.md");
std::fs::write(&json_path, json_out)
.with_context(|| format!("writing {}", json_path.display()))?;
std::fs::write(&ailx_path, ailx_out)
.with_context(|| format!("writing {}", ailx_path.display()))?;
eprintln!("xmodel-render: wrote {} and {}", json_path.display(), ailx_path.display());
std::fs::write(&ail_path, ail_out)
.with_context(|| format!("writing {}", ail_path.display()))?;
eprintln!("xmodel-render: wrote {} and {}", json_path.display(), ail_path.display());
Ok(())
}
@@ -2,20 +2,20 @@
//!
//! Recognises three directives, one per line, no nesting:
//! {form-only: json} … {/form-only}
//! {form-only: ailx} … {/form-only}
//! {form-only: ail} … {/form-only}
//! {example: <id>}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Form {
Json,
Ailx,
Ail,
}
impl Form {
fn parse(name: &str) -> Option<Form> {
match name {
"json" => Some(Form::Json),
"ailx" => Some(Form::Ailx),
"ail" => Some(Form::Ail),
_ => None,
}
}
@@ -39,7 +39,7 @@ fn round_trip_one(path: &Path) -> Result<(), String> {
let bytes_parsed = ailang_core::canonical::to_bytes(&parsed);
if bytes_original != bytes_parsed {
return Err(format!(
"{}: roundtrip diverged.\n--- printed AILX ---\n{}\n--- original bytes len {} vs parsed bytes len {} ---",
"{}: roundtrip diverged.\n--- printed AIL ---\n{}\n--- original bytes len {} vs parsed bytes len {} ---",
path.display(),
text,
bytes_original.len(),
@@ -50,7 +50,7 @@ fn round_trip_one(path: &Path) -> Result<(), String> {
}
#[test]
fn every_example_roundtrips_via_ailx() {
fn every_example_roundtrips_via_ail() {
let examples = list_examples();
assert!(
!examples.is_empty(),
@@ -22,13 +22,13 @@ fn form_only_block_is_isolated() {
}
#[test]
fn ailx_form_only_block_uses_ailx_form() {
let input = "{form-only: ailx}\ngrammar rule\n{/form-only}\n";
fn ail_form_only_block_uses_ail_form() {
let input = "{form-only: ail}\ngrammar rule\n{/form-only}\n";
let segments = split(input);
assert_eq!(
segments,
vec![
Segment::FormOnly { form: Form::Ailx, body: "grammar rule\n".to_string() },
Segment::FormOnly { form: Form::Ail, body: "grammar rule\n".to_string() },
]
);
}
@@ -49,7 +49,7 @@ fn example_marker_is_isolated() {
#[test]
fn multiple_directives_in_sequence() {
let input = "p1\n{example: a}\n{form-only: json}\njs\n{/form-only}\n{form-only: ailx}\nax\n{/form-only}\n{example: b}\np2\n";
let input = "p1\n{example: a}\n{form-only: json}\njs\n{/form-only}\n{form-only: ail}\nax\n{/form-only}\n{example: b}\np2\n";
let segments = split(input);
assert_eq!(
segments,
@@ -57,7 +57,7 @@ fn multiple_directives_in_sequence() {
Segment::Prose("p1\n".to_string()),
Segment::Example("a".to_string()),
Segment::FormOnly { form: Form::Json, body: "js\n".to_string() },
Segment::FormOnly { form: Form::Ailx, body: "ax\n".to_string() },
Segment::FormOnly { form: Form::Ail, body: "ax\n".to_string() },
Segment::Example("b".to_string()),
Segment::Prose("p2\n".to_string()),
]
@@ -1,7 +1,7 @@
//! Token-balance gate for master/spec.md form-only blocks.
//!
//! The mini-spec is the experimental treatment; if the JSON-cohort's
//! form-only scaffolding is meaningfully longer than the AILX-cohort's
//! form-only scaffolding is meaningfully longer than the AIL-cohort's
//! (or vice versa), the experiment is biased before the model ever
//! sees the prompts. This test catches gross imbalance.
@@ -20,27 +20,27 @@ fn form_only_block_token_totals_within_five_percent() {
let segments = splitter::split(&spec_text);
let mut json_buf = String::new();
let mut ailx_buf = String::new();
let mut ail_buf = String::new();
for seg in &segments {
if let Segment::FormOnly { form, body } = seg {
match form {
Form::Json => json_buf.push_str(body),
Form::Ailx => ailx_buf.push_str(body),
Form::Ail => ail_buf.push_str(body),
}
}
}
let bpe = tiktoken_rs::cl100k_base().expect("cl100k_base tokenizer should load");
let json_count = bpe.encode_with_special_tokens(&json_buf).len();
let ailx_count = bpe.encode_with_special_tokens(&ailx_buf).len();
let ail_count = bpe.encode_with_special_tokens(&ail_buf).len();
assert!(json_count > 0, "JSON-form blocks produced zero tokens");
assert!(ailx_count > 0, "AILX-form blocks produced zero tokens");
assert!(ail_count > 0, "AIL-form blocks produced zero tokens");
let max = json_count.max(ailx_count) as f64;
let diff = (json_count as i64 - ailx_count as i64).unsigned_abs() as f64;
let max = json_count.max(ail_count) as f64;
let diff = (json_count as i64 - ail_count as i64).unsigned_abs() as f64;
let ratio = diff / max;
assert!(
ratio <= 0.05,
"form-only block token imbalance: json={json_count}, ailx={ailx_count}, ratio={ratio:.4} > 0.05",
"form-only block token imbalance: json={json_count}, ail={ail_count}, ratio={ratio:.4} > 0.05",
);
}
@@ -24,7 +24,7 @@ The schema of the on-disk form is fixed. The toolchain rejects any
module that does not conform.
The AILX surface is parenthesised tagged-head Lisp-style notation.
The AIL surface is parenthesised tagged-head Lisp-style notation.
Every form begins with `(` followed by a tag identifier; positional
arguments follow; the form closes with `)`. There are no infix
operators, no operator precedence, no implicit conversions.
@@ -39,12 +39,12 @@ digit. String literals are double-quoted with `\"`, `\\`, `\n`, `\t`
escapes; integer literals are signed decimal; the `(unit)` keyword
denotes the unit value.
The AILX form is the printed dual of the JSON form: `ail parse
file.ailx` produces the same canonical bytes as the JSON form would,
and `ail render file.ail.json` produces the AILX text. Round-trip
The AIL form is the printed dual of the JSON form: `ail parse
file.ail` produces the same canonical bytes as the JSON form would,
and `ail render file.ail.json` produces the AIL text. Round-trip
through this pair is gated by an integration test.
```ailx
```ail
(module fn_returns_int
(fn answer
(doc "The simplest possible fn — no params, returns a fixed Int.")
@@ -95,7 +95,7 @@ Type::Forall: `(forall (a b) BODY-TYPE)` quantifies over `a` and
`b`. Class constraints attach as `(forall (a) (constraints (Eq a))
BODY-TYPE)`.
```ailx
```ail
(module forall_polymorphic
(fn id
(doc "The polymorphic identity function. Exercises Type::Forall and Type::Var.")
@@ -144,7 +144,7 @@ of the fn's signature and contributes to its content hash, so
making the choice explicit at authoring time avoids surprise
hash changes when the typechecker's defaults shift.
```ailx
```ail
(module param_modes_all
(fn f_implicit
(doc "Implicit mode is the legacy default — `param_modes` is omitted from canonical JSON when every entry is Implicit, which is the case here. The visitor still observes ParamMode::Implicit via the (defaulted) ret_mode field on Type::Fn.")
@@ -207,7 +207,7 @@ Inside any term position, an integer literal is a bare decimal
number, a string is a double-quoted string, a bool is `true` or
`false`, unit is `(unit)`.
```ailx
```ail
(module fn_calls_prelude
(fn add
(doc "Add two Ints via the polymorphic prelude `+`. Also exercises TermLet by binding the sum to a local name before returning it, and TermClone by re-using the let-bound value.")
@@ -216,7 +216,7 @@ number, a string is a double-quoted string, a bool is `true` or
(body (let s (app + x y) (clone s)))))
```
```ailx
```ail
(module fn_with_lambda
(fn make_adder
(doc "Curried adder: takes an Int and returns an Int -> Int closure. Exercises TermLam and Type::Fn appearing as a return type.")
@@ -258,7 +258,7 @@ need a separate registry to resolve overlapping constructor names
across ADTs. Example: `(ctor List Cons 1 (ctor List Nil))` builds a
single-element list.
```ailx
```ail
(module data_simple
(data Box (vars a)
(doc "A unary box around a polymorphic value.")
@@ -307,7 +307,7 @@ Pattern variables introduced by a `(case PAT BODY)` are in scope
inside `BODY` and bind there only. They do not leak into sibling
arms or into code outside the `match`.
```ailx
```ail
(module data_with_match
(data List
(doc "Monomorphic singly-linked Int list — boxed, recursive.")
@@ -329,7 +329,7 @@ arms or into code outside the `match`.
(case (pat-ctor Cons _ t) (app + 1 (app go t))))) (in (app go xs))))))
```
```ailx
```ail
(module match_literal_pattern
(fn classify
(doc "Classify an Int via literal patterns plus a wildcard fallback. Exercises PatternLit and PatternWild.")
@@ -382,7 +382,7 @@ value; `X`'s value is discarded. Chains of sequencing are written
nested: `(seq A (seq B C))` runs `A`, then `B`, then yields `C`'s
value.
```ailx
```ail
(module fn_with_do_seq
(fn main
(doc "Print two Ints in sequence. Exercises TermDo, TermSeq, and the IO effect on a fn type. The trailing unit return is implicit in the last Do (op returns Unit).")
@@ -398,10 +398,10 @@ There are five literal shapes. An integer literal is a signed
literal is a UTF-8 sequence in double quotes; AILang restricts the
authored byte set to ASCII printable (Decision 6 Constraint 3) — non-
ASCII bytes are an authoring error. A unit literal is the value of
type Unit, written `(unit)` in AILX and `{"kind": "unit"}` in JSON.
type Unit, written `(unit)` in AIL and `{"kind": "unit"}` in JSON.
A Float literal is an IEEE-754 binary64 value. When you author
Floats, write the decimal form (`3.14`) — the AILX parser converts
Floats, write the decimal form (`3.14`) — the AIL parser converts
to the 64-bit bit pattern and emits the canonical 16-character
lowercase hex string into the JSON. You never author the hex
encoding by hand; the toolchain owns that representation. NaN and
@@ -431,7 +431,7 @@ There is no separate syntax for exponent-form floats in the v1
authoring surface; for non-finite values use the prelude constants
`nan`, `inf`, `neg_inf`.
```ailx
```ail
(module floats
(const pi
(doc "Pi as a Float constant. Exercises Def::Const and Literal::Float — the bit pattern below is f64::to_bits(3.14).")
@@ -439,7 +439,7 @@ authoring surface; for non-finite values use the prelude constants
(body 3.14)))
```
```ailx
```ail
(module bool_str
(fn is_true
(doc "Trivial Bool literal. Exercises Literal::Bool.")
@@ -498,7 +498,7 @@ a)) BODY-TYPE)`. Each constraint is `(CLASS-NAME TYPE)` — usually
the type is a single bound type variable but a concrete type is
also legal (typically a typecheck error unless an instance exists).
```ailx
```ail
(module class_def
(class MyShow
(param a)
@@ -507,7 +507,7 @@ also legal (typically a typecheck error unless an instance exists).
(type (fn-type (params a) (ret (con Str)))))))
```
```ailx
```ail
(module instance_def
(class MyShow
(param a)
@@ -733,10 +733,10 @@ There are five literal shapes. An integer literal is a signed
literal is a UTF-8 sequence in double quotes; AILang restricts the
authored byte set to ASCII printable (Decision 6 Constraint 3) — non-
ASCII bytes are an authoring error. A unit literal is the value of
type Unit, written `(unit)` in AILX and `{"kind": "unit"}` in JSON.
type Unit, written `(unit)` in AIL and `{"kind": "unit"}` in JSON.
A Float literal is an IEEE-754 binary64 value. When you author
Floats, write the decimal form (`3.14`) — the AILX parser converts
Floats, write the decimal form (`3.14`) — the AIL parser converts
to the 64-bit bit pattern and emits the canonical 16-character
lowercase hex string into the JSON. You never author the hex
encoding by hand; the toolchain owns that representation. NaN and
@@ -761,7 +761,7 @@ Literal variants and their JSON shapes:
serialisation drift across `serde_json` versions.
Float bit patterns are computed by the toolchain on `ail parse`,
not by the author. When you write `3.14` in AILX, the parser emits
not by the author. When you write `3.14` in AIL, the parser emits
`"bits": "40091eb851eb851f"` in the JSON form.
+2 -2
View File
@@ -19,7 +19,7 @@ The system was bootstrapped on 2026-05-09. See
| [`implement`](implement/SKILL.md) | Plan exists | Code + tests + per-iter journal entry, all uncommitted in the working tree | Standard iteration path |
| [`audit`](audit/SKILL.md) | Milestone closing OR baseline drift suspected | Drift report + bench-regression report | **Mandatory** at milestone close |
| [`docwriter`](docwriter/SKILL.md) | API surface stabilized; rustdoc lag suspected | rustdoc updates in `///` and `//!`, uncommitted in the working tree | No — Boss-dispatched only |
| [`fieldtest`](fieldtest/SKILL.md) | Boss-dispatched post-audit field test on a milestone that touched user-visible surface | 2-4 `.ailx` example fixtures + `docs/specs/<date>-fieldtest-<milestone>.md`, all uncommitted in the working tree | No — Boss-dispatched only |
| [`fieldtest`](fieldtest/SKILL.md) | Boss-dispatched post-audit field test on a milestone that touched user-visible surface | 2-4 `.ail` example fixtures + `docs/specs/<date>-fieldtest-<milestone>.md`, all uncommitted in the working tree | No — Boss-dispatched only |
| [`debug`](debug/SKILL.md) | Bug encountered (failing test, segfault, wrong stdout) | RED-test in the working tree (uncommitted) + cause analysis | **Mandatory RED-first** for any bug |
| [`boss`](boss/SKILL.md) | User-invoked only (`/boss`) | Autonomous orchestration session — dispatches existing skills until done-state or bounce-back | No — user-gated |
@@ -79,7 +79,7 @@ are no orphan agents (an anti-pattern after the 2026-05-09 build-out).
| `ailang-bencher` | `audit/agents/` | `audit` (regression diagnostics — hypothesis-driven) |
| `ailang-docwriter` | `docwriter/agents/` | `docwriter` (Boss-dispatched rustdoc sweep post-stability) |
| `ailang-debugger` | `debug/agents/` | `debug` (RED-first; hands off GREEN to `implement` mini-mode) |
| `ailang-fieldtester` | `fieldtest/agents/` | `fieldtest` (writes real-world examples in `.ailx` Surface form against DESIGN.md only — never the compiler source) |
| `ailang-fieldtester` | `fieldtest/agents/` | `fieldtest` (writes real-world examples in `.ail` Surface form against DESIGN.md only — never the compiler source) |
Each agent file has YAML frontmatter (`name`, `description`, `tools`)
plus a system-prompt body. The `description` field is the one-sentence
+1 -1
View File
@@ -130,7 +130,7 @@ is a side experiment, not the headline.
## What you DO ship
- New bench fixtures under `examples/bench_*.{ailx,ail.json}` when none of
- New bench fixtures under `examples/bench_*.ail*` when none of
the existing ones exercise the hypothesis. Pair them (Implicit + explicit-mode
variants) where the comparison demands it.
- Edits to `bench/run.sh` (or a new harness alongside it) when the
+5 -5
View File
@@ -1,6 +1,6 @@
---
name: fieldtest
description: Boss-dispatched only, after audit closes clean (or with ratified drift only), when the orchestrator judges the iteration is complete and wants a field test. Picks 2-4 real-world programming tasks within the milestone's scope, implements each in the AIL Surface form (.ailx — not raw JSON), runs the resulting binaries, and writes a friction-and-bug spec to docs/specs/<date>-fieldtest-<milestone>.md. The spec feeds the next plan as a reference. Implementer simulates a downstream LLM that has only DESIGN.md plus the public examples — never the language's own implementation.
description: Boss-dispatched only, after audit closes clean (or with ratified drift only), when the orchestrator judges the iteration is complete and wants a field test. Picks 2-4 real-world programming tasks within the milestone's scope, implements each in the AIL Surface form (.ail — not raw JSON), runs the resulting binaries, and writes a friction-and-bug spec to docs/specs/<date>-fieldtest-<milestone>.md. The spec feeds the next plan as a reference. Implementer simulates a downstream LLM that has only DESIGN.md plus the public examples — never the language's own implementation.
---
# fieldtest — LLM-usability field test for a shipped milestone
@@ -24,7 +24,7 @@ specs at `docs/specs/<date>-fieldtest-<milestone>.md`.
The substantive process — read DESIGN.md + JOURNAL + milestone spec,
pick 2-4 real-world programming tasks per milestone axis, implement
each in `.ailx` Surface form, run via `ail check`/`build`/`run`,
each in `.ail` Surface form, run via `ail check`/`build`/`run`,
classify findings, write the spec — lives in
`agents/ailang-fieldtester.md`. That file also carries the spec
template, the source-isolation discipline (no reading under
@@ -65,7 +65,7 @@ routing table below applies in all cases.
```
THE FIELDTESTER WORKS FROM DESIGN.MD AND PUBLIC EXAMPLES — NOT FROM THE COMPILER SOURCE.
EVERY EXAMPLE IS WRITTEN IN .ailx (SURFACE) FIRST. RAW .ail.json IS NEVER HAND-AUTHORED.
EVERY EXAMPLE IS WRITTEN IN .ail (SURFACE) FIRST. RAW .ail.json IS NEVER HAND-AUTHORED.
EVERY FRICTION POINT AND BUG IS RECORDED. NONE IS WORKED AROUND.
```
@@ -79,7 +79,7 @@ agent compiler-internal hints in the carrier.
Dispatch `ailang-fieldtester` with the carrier from the Handoff
Contract below. The agent picks 2-4 examples (one per axis the
milestone touched), implements them in `.ailx`, runs them through the
milestone touched), implements them in `.ail`, runs them through the
public `ail` CLI, classifies findings, and writes the spec. All
artefacts (fixtures + spec) stay in the working tree as unstaged
changes; the Boss commits them after reviewing the report (suggested
@@ -104,7 +104,7 @@ variation); five is too many for one report to stay readable.
| Field | Content |
|-------|---------|
| `spec_path` | `docs/specs/<date>-fieldtest-<milestone>.md` |
| `examples_added` | list of `.ailx` paths committed |
| `examples_added` | list of `.ail` paths committed |
| `findings` | list, each with class (`bug` / `friction` / `spec_gap` / `working`) + recommendation |
| `status` | `clean` / `friction_found` / `bugs_found` / `infra_blocked` |
+15 -15
View File
@@ -1,6 +1,6 @@
---
name: ailang-fieldtester
description: Implements 2-4 real-world programming tasks in the AIL Surface form (.ailx) for a freshly closed milestone, runs them through the public `ail` CLI, and reports friction, bugs, and spec gaps as a structured spec. Simulates a downstream LLM author who has only DESIGN.md and the public examples — never the language's own implementation. Does NOT fix bugs and does NOT hand-write canonical JSON.
description: Implements 2-4 real-world programming tasks in the AIL Surface form (.ail) for a freshly closed milestone, runs them through the public `ail` CLI, and reports friction, bugs, and spec gaps as a structured spec. Simulates a downstream LLM author who has only DESIGN.md and the public examples — never the language's own implementation. Does NOT fix bugs and does NOT hand-write canonical JSON.
tools: Read, Edit, Write, Bash, Glob, Grep
---
@@ -46,7 +46,7 @@ Read in this order, before picking examples:
4. `docs/specs/<milestone>.md` if one exists — the contract this
milestone signed up for.
5. `examples/` — to learn the *form* of valid AIL. You may read any
`.ailx` and `.ail.json` under `examples/` (these are the public
`.ail` and `.ail.json` under `examples/` (these are the public
corpus). You may NOT use them as a hint about how the compiler
handles edge cases; only as a hint about the shape of the surface.
@@ -66,7 +66,7 @@ spec; if both are also empty, return `NEEDS_CONTEXT`.
```
DESIGN.MD AND `examples/` ARE YOUR ONLY REFERENCE. CRATES/, RUNTIME/, BENCH/ ARE FORBIDDEN READS.
EVERY EXAMPLE IS WRITTEN IN .ailx FIRST. NO HAND-WRITTEN .ail.json.
EVERY EXAMPLE IS WRITTEN IN .ail FIRST. NO HAND-WRITTEN .ail.json.
RECORD WHAT HAPPENS. DO NOT FIX. DO NOT WORK AROUND.
```
@@ -76,7 +76,7 @@ under `crates/`, `runtime/`, `bench/scripts/`, or `bench/reference/`,
**stop**. The only file paths you may open are:
- `CLAUDE.md`, `docs/**`, `examples/**`, `skills/**`
- the `.ailx` and `.ail.json` files YOU create under `examples/`
- the `.ail` and `.ail.json` files YOU create under `examples/`
- the binaries YOU produce via `ail build`
- the `.ll` files YOU produce via `ail emit-ir` if you want to
inspect generated IR (the IR is part of the public surface per
@@ -104,19 +104,19 @@ Each phase completes before the next starts.
thin) or a 500-line numerics library (too thick).
3. Total: 2-4 examples.
### Phase 2 — Implement each example in `.ailx`
### Phase 2 — Implement each example in `.ail`
For each example, in this order:
1. Draft the program in `.ailx` Surface form. Reach for the milestone's
1. Draft the program in `.ail` Surface form. Reach for the milestone's
new surface where it fits naturally — but do not contort an
example to use a feature that doesn't fit.
2. Save as `examples/fieldtest/<milestone-short>_<n>_<slug>.ailx`,
e.g. `examples/fieldtest/22_1_eq_rational.ailx`.
2. Save as `examples/fieldtest/<milestone-short>_<n>_<slug>.ail`,
e.g. `examples/fieldtest/22_1_eq_rational.ail`.
3. Run, in this order:
```bash
ail check examples/fieldtest/<...>.ailx
ail build examples/fieldtest/<...>.ailx -o /tmp/ft_<n>
ail check examples/fieldtest/<...>.ail
ail build examples/fieldtest/<...>.ail -o /tmp/ft_<n>
/tmp/ft_<n>
```
Note: if your repo invokes `ail` as `cargo run -p ail --` instead,
@@ -137,10 +137,10 @@ binary missing); in that case return `BLOCKED` with the cause.
### Phase 3 — Generate canonical JSON, only via tooling
The canonical form is `.ail.json`. You do NOT hand-write it. After
each `.ailx` runs cleanly, generate the JSON via:
each `.ail` runs cleanly, generate the JSON via:
```bash
ail render --json examples/fieldtest/<...>.ailx > examples/fieldtest/<...>.ail.json
ail render --json examples/fieldtest/<...>.ail > examples/fieldtest/<...>.ail.json
```
(or whichever subcommand `ail --help` lists for the surface→json
@@ -164,7 +164,7 @@ merged.
### Phase 5 — Write the spec, hand back
Write `docs/specs/<YYYY-MM-DD>-fieldtest-<milestone>.md` using the
spec structure below. Leave all artefacts (the `.ailx` files, the
spec structure below. Leave all artefacts (the `.ail` files, the
`.ail.json` files, and the spec file) in the working tree as
unstaged changes. You do NOT commit — the Boss commits after
reading the end-report (suggested commit subject:
@@ -187,7 +187,7 @@ What the milestone shipped. One paragraph.
## Examples
Per example, one subsection:
### `examples/fieldtest/<milestone>_<n>_<slug>.ailx` — <task name>
### `examples/fieldtest/<milestone>_<n>_<slug>.ail` — <task name>
- What it does
- Why this task fits the milestone's scope
- Outcome: compiles? runs? matches expected stdout?
@@ -272,7 +272,7 @@ before committing.
| "DESIGN.md is fuzzy on the typeclass instance ordering, I'll pick the natural reading and proceed" | Pick the reading, RUN the example, AND record `spec_gap` with the reading you picked and why another reading was equally plausible. |
| "Bug found — I'll just fix it now, faster than handing off to debug" | Fix-in-place violates the skill split. The fix lands in a separate, RED-tested commit via `debug` → `implement`. |
| "Two examples both ran clean, no findings — short report" | A clean run is itself a finding (`working`). Record what was reached for, what diagnostic showed up when wrong, what was easy. Wins protect the feature from drift. |
| "I'll skip the JSON file generation, the .ailx is enough" | The committed pair (.ailx + .ail.json) is the regression fixture. Without the JSON, future bench runs can't pick it up. If `ail render --json` doesn't exist, record it as a finding. |
| "I'll skip the JSON file generation, the .ail is enough" | The committed pair (.ail + .ail.json) is the regression fixture. Without the JSON, future bench runs can't pick it up. If `ail render --json` doesn't exist, record it as a finding. |
| "Three examples is enough, I'll skip the fourth axis" | Each axis the milestone touched needs at least one example. Skipping an axis silently turns the field test into a partial signal, which is worse than no signal because the orchestrator will read it as full coverage. |
## Red Flags — STOP and re-read DESIGN.md