Files
AILang/docs/journals/2026-05-12-iter-ext-cli.1.md
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

6.6 KiB
Raw Blame History

iter ext-cli.1 — CLI accepts .ail (Form A) source files alongside .ail.json

Date: 2026-05-12 Started from: efecbaa Status: DONE Tasks completed: 4 of 4

Summary

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; the binary semantics are extension-agnostic.

Architecture: a new ailang_surface::{load_module, load_workspace} pair dispatches on file extension. A new injection point in ailang_core::workspace::load_workspace_with<F> lets the surface crate plug in an extension-aware loader without ailang-core having to import ailang-surface (which would close a crate cycle). Import resolution in workspace::visit now prefers a .ail sibling over a .ail.json sibling of the same module name. The CLI binary's 18 ailang_core::load_* callsites all switched to the new ailang_surface::load_*. Surface parse failures route through the existing workspace_error_to_diagnostic channel 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 pre-iter misleading json: expected value at line 1 column 1 fall-through.

Per-task notes

  • iter ext-cli.1.1: WorkspaceLoadError::SurfaceParse { path, message } variant added to ailang_core::workspace; load_workspace refactored into a thin wrapper around a new pub fn load_workspace_with<F>(entry, loader: F) with F: Fn(&Path) -> Result<Module, WorkspaceLoadError> + Copy; visit's signature gained the loader parameter and its import-path construction now does .ail-first / .ail.json-fallback; new test load_workspace_with_custom_loader_is_called pins the injection. tempfile added as ailang-core dev-dependency.
  • iter ext-cli.1.2: new crates/ailang-surface/src/loader.rs with pub fn load_module (extension-dispatching: .ailparse, else delegate to ailang_core::load_module) and pub fn load_workspace (calls load_workspace_with(entry, load_module)); lib.rs re-exports both. Three integration tests in crates/ailang-surface/tests/loader.rs pin the dispatch on each form individually + the mixed-extension workspace case. tempfile added as ailang-surface dev-dependency.
  • iter ext-cli.1.3: every ailang_core::load_module / ailang_core::load_workspace callsite in crates/ail/src/main.rs (18 sites total) replaced with ailang_surface::load_module / ailang_surface::load_workspace. Workspace-build placeholder W::SurfaceParse { .. } => None arm added to workspace_error_to_diagnostic (replaced in Task 4) so the exhaustive match compiles. Two new E2E tests in crates/ail/tests/e2e.rs: ail_check_accepts_ail_source and ail_run_accepts_ail_source_with_same_stdout_as_ail_json.
  • iter ext-cli.1.4: placeholder arm replaced with proper W::SurfaceParse { path, message } => Some(Diagnostic::error( "surface_parse_error", message).with_ctx({"path": ...})) — mirroring the SchemaMismatch arm's builder pattern. DESIGN.md §Decision 6 / "CLI" bullet gained the one-paragraph addendum (verbatim from the plan, dated). New ct1 test ail_check_json_on_ail_with_syntax_error_returns_structured_diagnostic pins the JSON-diagnostic shape.

Plus one E2E coverage test added during Phase 3: load_workspace_prefers_ail_when_both_extensions_exist — protects the .ail-first precedence when both extensions are present for the same module (the existing 1.2 test only exercised the fallback half).

Concerns

  • Diagnostic code convention drift (Boss-corrected pre-commit). The plan-specified code surface_parse_error used snake_case; every other diagnostic code in the codebase is kebab-case (type-mismatch, unbound-var, over-strict-mode, reuse-as-shape-mismatch, ...). Boss verified the convention via git grep over crates/ailang-check/src and crates/ail/src before commit and aligned the literal to surface-parse-error at three sites: the SurfaceParse arm in workspace_error_to_diagnostic (crates/ail/src/main.rs) and the two matching strings in crates/ail/tests/ct1_check_cli.rs (the rustdoc on the test + the assert_eq! against first["code"]). The plan file is left at the snake_case form as historical record; the source of truth is now the corrected code.
  • bench/check.py first run noisy. First execution showed 4 regressions on latency.{explicit,implicit}_at_rc.{p99_9,max}_us (tail-latency tier). Re-runs (×2) returned 0 regressed (one even showed 4 improvements beyond tolerance). This is the same latency.explicit_at_rc.* cluster audit-cma and audit-ms already documented as nondeterministic; the iter's actual code change introduces ONE extra surface_path.is_file() syscall per import-resolution step plus a single-byte extension check per load, neither of which can move us-scale tail latencies. Net: baseline left pristine, consistent with the last two audits.

Known debt

  • The ail parse subcommand is still the only way to materialise a stable .ail.json snapshot from a .ail source. Now that every subcommand silently dispatches .ail through the parser, one might argue for a "did you mean ail parse?" hint on paths with non-.ail/non-.ail.json extensions. The roadmap note for ext-cli.1 mentions this as a possible same-iter or follow-up scope; nothing surfaced as load-bearing during impl, deferring.

Files touched

Code:

  • crates/ailang-core/Cargo.toml (+ tempfile dev-dep)
  • crates/ailang-core/src/workspace.rs (SurfaceParse variant, load_workspace_with, visit threading, .ail-first precedence, new unit test)
  • crates/ailang-surface/Cargo.toml (+ tempfile dev-dep)
  • crates/ailang-surface/src/lib.rs (mod loader + re-exports)
  • crates/ailang-surface/src/loader.rs (NEW)
  • crates/ailang-surface/tests/loader.rs (NEW; 4 tests)
  • crates/ail/src/main.rs (18 callsite rewires + SurfaceParse diagnostic arm)
  • crates/ail/tests/e2e.rs (2 new E2E tests)
  • crates/ail/tests/ct1_check_cli.rs (1 new diagnostic-shape test)

Spec / metadata:

  • docs/DESIGN.md (§Decision 6 / CLI bullet addendum)
  • Cargo.lock (tempfile dev-dep transitive)

Stats

bench/orchestrator-stats/2026-05-12-iter-ext-cli.1.json