Closes the loop opened by this morning's .ailx → .ail rename. Every
path-taking ail subcommand (check, build, run, manifest, render,
prose, merge-prose, describe, emit-ir, diff, workspace, deps) now
accepts a .ail (Form A) input as well as .ail.json (Form B). For
.ail paths the subcommand parses through ailang_surface::parse
in-line and proceeds with the same loaded Module value .ail.json
would have produced; binary semantics are extension-agnostic.
Architecture: a new ailang_surface::{load_module, load_workspace}
pair dispatches on file extension. A new injection point
ailang_core::workspace::load_workspace_with<F> lets the surface
crate plug in an extension-aware loader without core having to
import surface (which would close a crate cycle). Import resolution
in workspace::visit now prefers a .ail sibling over a .ail.json
sibling. The CLI binary's 18 callsites switched to the surface
loaders. Surface parse failures route through
workspace_error_to_diagnostic as a new SurfaceParse variant of
WorkspaceLoadError, so `ail check --json foo.ail` on a malformed
.ail returns a parseable JSON diagnostic with code
surface-parse-error instead of the misleading
`json: expected value at line 1 column 1` fall-through.
Boss pre-commit correction: the implementer followed the plan
verbatim and used snake_case `surface_parse_error`, but every
existing diagnostic code in the codebase is kebab-case (verified
via `git grep` over crates/ailang-check/src and crates/ail/src);
realigned to `surface-parse-error` at three sites (the diagnostic
arm + two ct1 test strings). Journal Concerns section records
the correction.
Tests: cargo test --workspace 487 green (+7 from this iter:
1 core unit, 4 surface integration, 1 ail E2E pair, 1 ct1 CLI
diagnostic-shape). bench/check.py, compile_check.py, cross_lang.py
clean on re-run; the latency.{explicit,implicit}_at_rc tail
cluster occasionally flagged on first runs is the same
nondeterministic cluster the previous two audits documented;
baseline left pristine for the third consecutive iter.
Roadmap follow-up "ail check/build/run accept .ail extension" (P2)
removed.
DESIGN.md §Decision 6 / "CLI" bullet gained the symmetric upward
sentence: both .ail and .ail.json are first-class CLI inputs.
Three new reader-only tests pin the .ail.json / .ailx bijection
plus AST-variant coverage. All three passed first observation
across the current corpus — no roundtrip, schema-coverage, or
CLI-drift gaps surfaced.
- every_ailx_fixture_matches_its_json_counterpart (replaces the
3-pair handwritten exhibits): 57 paired .ailx fixtures pass.
- every_ast_variant_is_observed_in_the_fixture_corpus (new): 34/34
AST variants exercised across 136 fixtures; exhaustive matches
without _ wildcard so AST drift fails the build.
- cli_render_then_parse_preserves_canonical_bytes_on_every_fixture
(new): 136 fixtures round-trip through ail render → tempfile →
ail parse with BLAKE3 identity on canonical bytes.
No production code, no DESIGN.md changes (those follow in later
iterations). Tests are pure readers of the repo per spec
acceptance #7 — tempfile crate added as workspace dep for the CLI
roundtrip's intermediate file outside the repo.
Adds a per-fn linearity check that runs on every fn whose
param_modes are all explicit (Borrow or Own, no Implicit), tracks
each binder's consume/borrow state through the AST, and emits
two diagnostics with form-A `suggested_rewrites`:
- `use-after-consume` — a binder is referenced after it has
already been consumed.
- `consume-while-borrowed` — a binder is consumed while a borrow
of it is still live (sibling arg slot, or an enclosing-fn
Borrow param).
Fns with any Implicit param skip the check entirely; that's the
back-compat lane that keeps every existing fixture green. Pure
diagnostic addition: no IR change, no codegen change, no runtime
change.
Each diagnostic carries a non-empty `suggested_rewrites` whose
`replacement` is form-A AILang text (typically `(clone <name>)`).
Adds two new public entrypoints to ailang-surface — `parse_term`
and `term_to_form_a` — so the round-trip
`parse_term(term_to_form_a(t)) ≡ t` is the contract every
emitted replacement must satisfy. Tests assert the contract.
Linearity pass runs only on modules that typechecked clean (any
upstream typecheck error suppresses it for that module — running
on partly-defined IR would produce noise).
Tests: 3 unit tests in `linearity::tests`, 3 integration tests
in `ailang-check/tests/workspace.rs`, 1 serialisation test in
`diagnostic.rs`. `cargo test --workspace` green.
Strictly additive new crate per Decision 6 architectural pin.
JSON-AST stays the source of truth; ailang-surface is one
producer/consumer of ailang_core::ast::Module values. ailang-check
and ailang-codegen unchanged.
Crate contents:
- src/lex.rs (~264 LOC): 3-rule lexical core. Whitespace and parens
delimit tokens; semicolon to EOL is comment; first-character
classifier (digit -> int, " -> string, else -> ident).
- src/parse.rs (~1041 LOC): hand-written recursive descent. One Rust
fn per EBNF production. No parser-combinator dep.
- src/print.rs (~371 LOC): deterministic pretty-printer. Round-trip
contract with parse() is the surface's correctness gate.
- tests/round_trip.rs (~128 LOC): integration test runs every
examples/*.ail.json fixture through print -> parse -> canonical
JSON, asserts canonical-byte equality with the original.
Two AST-driven form widenings beyond the 14b sketch (both folded
into DESIGN.md Decision 6):
- lam-term carries (typed name type) params, ret type, and
optional effects (Term::Lam has parallel param_tys/ret_ty/
effects fields).
- import-clause admits (import name (as alias)?) (Import.alias
is Option<String>).
Production count ~28, under 30-rule budget. Constraint 1
(formalisable for foreign LLM) intact.
Verification:
- cargo build --workspace green.
- cargo test --workspace: 76 tests green (was 64; +9 surface unit,
+2 round-trip integration). All 17 fixtures round-trip
byte-identical at canonical level. 3 hand-written .ailx
exhibits parse to canonical JSON identical to their .ail.json
siblings.
- cargo doc --no-deps zero warnings (DESIGN.md item 6 invariant).
Manual smoke test (ail parse → ail run): hello, box,
list_map_poly all produce expected output through the form-(A)
authoring lane end-to-end.
CLI: ail parse <file.ailx> [-o <file.ail.json>]. .ail.json
remains a first-class input to every existing subcommand.
Plan 14d: stdlib (std_list.ailx with length/filter/fold/concat/
reverse/head/tail), authored in form (A) from day one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neue Top-Level-API check_module(&Module) -> Vec<Diagnostic> in
ailang-check, plus stabile Codes (unbound-var, type-mismatch,
arity-mismatch, non-exhaustive-match, unknown-ctor-in-pattern,
duplicate-def, …). CLI bekommt --json-Flag für maschinenlesbares
Output, Exit 1 bei Errors. Text-Modus unverändert. E2E-Test
check_json_unbound_var sichert das Format ab.
Erste lauffähige Iteration. examples/sum.ail.json wird zu nativem Binary
kompiliert und druckt 55 (Summe 1..10) als End-to-End-Test.
Architektur:
- ailang-core: hashbares JSON-AST + canonical-form + pretty-printer
- ailang-check: monomorpher HM-Subset + Effekt-Set-Tracking
- ailang-codegen: LLVM-IR-Text-Emitter (kein libllvm-link)
- ail: CLI mit check/manifest/render/describe/emit-ir/build/builtins
Designentscheidungen sind in docs/DESIGN.md dokumentiert; der Verlauf
in docs/JOURNAL.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>