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.
This commit is contained in:
@@ -32,8 +32,10 @@
|
||||
//! 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};
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//! Extension-dispatching loader for AILang source files.
|
||||
//!
|
||||
//! AILang has two file forms today: Form A (`.ail`, the LLM
|
||||
//! authoring surface) and Form B (`.ail.json`, the canonical
|
||||
//! JSON-AST). These loaders accept both: for `.ail` they read the
|
||||
//! source text and parse it via [`crate::parse`]; for `.ail.json`
|
||||
//! they delegate to [`ailang_core::load_module`].
|
||||
//!
|
||||
//! These functions live in `ailang-surface` rather than
|
||||
//! `ailang-core` because `ailang-core` cannot import `ailang-surface`
|
||||
//! without creating a circular crate dependency. The cross-crate
|
||||
//! injection point is [`ailang_core::workspace::load_workspace_with`],
|
||||
//! introduced in iter ext-cli.1.
|
||||
|
||||
use ailang_core::ast::Module;
|
||||
use ailang_core::workspace::{load_workspace_with, Workspace, WorkspaceLoadError};
|
||||
use std::path::Path;
|
||||
|
||||
fn is_ail_source(path: &Path) -> bool {
|
||||
path.extension().and_then(|s| s.to_str()) == Some("ail")
|
||||
}
|
||||
|
||||
/// Load a single module from either `.ail` or `.ail.json`.
|
||||
///
|
||||
/// Dispatches on file extension:
|
||||
///
|
||||
/// - `.ail` — read source text, parse via [`crate::parse`], return
|
||||
/// the in-memory [`Module`].
|
||||
/// - anything else — delegate to [`ailang_core::load_module`] and
|
||||
/// convert its `Error` into the corresponding
|
||||
/// [`WorkspaceLoadError`] variant.
|
||||
///
|
||||
/// Errors are returned as [`WorkspaceLoadError`] so that single-
|
||||
/// module callers and workspace callers can share the same error
|
||||
/// type (and `workspace_error_to_diagnostic` in the binary can
|
||||
/// route them through one channel).
|
||||
pub fn load_module(path: &Path) -> Result<Module, WorkspaceLoadError> {
|
||||
if is_ail_source(path) {
|
||||
let src = std::fs::read_to_string(path).map_err(|e| WorkspaceLoadError::Io {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
crate::parse(&src).map_err(|e| WorkspaceLoadError::SurfaceParse {
|
||||
path: path.to_path_buf(),
|
||||
message: format!("{e}"),
|
||||
})
|
||||
} else {
|
||||
match ailang_core::load_module(path) {
|
||||
Ok(m) => Ok(m),
|
||||
Err(ailang_core::Error::Io(e)) => Err(WorkspaceLoadError::Io {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
}),
|
||||
Err(e) => Err(WorkspaceLoadError::Schema {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a workspace from an entry path, accepting either `.ail` or
|
||||
/// `.ail.json` for the entry and for every transitive import.
|
||||
///
|
||||
/// Import resolution prefers a sibling `<module>.ail` over
|
||||
/// `<module>.ail.json`; this is the new precedence rule baked into
|
||||
/// `core::workspace::visit`. Mixed-extension workspaces (entry is
|
||||
/// `.ail`, some imports are `.ail.json`) are valid.
|
||||
pub fn load_workspace(entry: &Path) -> Result<Workspace, WorkspaceLoadError> {
|
||||
load_workspace_with(entry, load_module)
|
||||
}
|
||||
Reference in New Issue
Block a user