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 @@
|
||||
# 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.
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user