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:
@@ -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,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,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.
|
||||
|
||||
@@ -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,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).
|
||||
//
|
||||
|
||||
@@ -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()))?;
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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;
|
||||
|
||||
+1
-1
@@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
+19
-19
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user