Files
AILang/crates/ailang-surface/src/lib.rs
T
Brummel 79cc78507b iter ext-cli.1: CLI accepts .ail (Form A) sources alongside .ail.json
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.
2026-05-12 14:45:03 +02:00

42 lines
1.7 KiB
Rust

//! AILang authoring surface — form (A) S-expression projection.
//!
//! This crate is **one of potentially many** producers / consumers of
//! [`ailang_core::ast::Module`] values. The JSON-AST in `ailang-core`
//! remains the canonical, hashable, content-addressed source of truth;
//! form (A) is the AI-authoring projection optimised for token-efficient
//! production by LLMs (see `docs/DESIGN.md` Decision 6).
//!
//! ## Modules
//!
//! - [`mod@lex`] — hand-written lexer for the 3-rule lexical core
//! (whitespace / parens / comments delimit tokens; first character
//! classifies into integer / string / ident).
//! - [`mod@parse`] — hand-written recursive-descent parser. One Rust
//! function per EBNF production, line-by-line auditable. No
//! parser-combinator dependency.
//! - [`mod@print`] — deterministic pretty-printer that emits form (A).
//! Round-trip with the parser is the contract that gates the
//! surface (see the integration test `tests/round_trip.rs`).
//!
//! ## Round-trip contract
//!
//! For every well-formed [`ailang_core::ast::Module`] value `m`,
//! [`parse()`] applied to [`print()`]`(m)` produces a module whose
//! canonical JSON bytes equal those of `m`. This is enforced by the
//! integration test against every `examples/*.ail.json` fixture.
//!
//! ## Architectural pin
//!
//! No new AST nodes, no schema changes, no new hashable form.
//! `ailang-check` and `ailang-codegen` are projection-agnostic — they
//! consume `Module` values regardless of which front-end produced them.
pub mod lex;
pub mod loader;
pub mod parse;
pub mod print;
pub use loader::{load_module, load_workspace};
pub use parse::{parse, parse_term, ParseError};
pub use print::{print, term_to_form_a, type_to_form_a};