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,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.