Files
AILang/crates/ailang-surface/src/loader.rs
T
Brummel 008d18bb18 iter pd.2: surface owns prelude embed; pd.1 shim retired
Moved PRELUDE_AIL const + parse_prelude fn to ailang-surface;
re-exported from surface's lib.rs. surface::load_workspace rewritten
3-line shim-call → 7-line composition: load_modules_with → reserved-name
check → inject parse_prelude → build_workspace(&["prelude"]).

Five deletions in core: PRELUDE_JSON, load_prelude, load_workspace_with
(the pd.1 shim), the top-level cfg(test) load_one fn, and the
cfg(test) crate::load_module import. Production literal-"prelude" count
in crates/ailang-core/src/ is now zero. Cross-form-identity preflight
PASSED at module_hash 3abe0d3fa3c11c99 — parse(prelude.ail) ≡
deserialize(prelude.ail.json), so pd.3's deletion of the JSON is safe.

Three new pin tests in ailang-surface/tests/: prelude_parse_pin (succeeds
+ min defs); prelude_module_hash_pin (cross-form-identity + literal-hex
anchor); prelude_injection_pin (inject + reservation).

Plan deviations (recorded in journal): switched the regression-pin
fixture from non-existent examples/hello.ail.json to hello.ail; relocated
10 in-mod core tests to crates/ailang-core/tests/workspace_pin.rs
because the dev-dep cycle (core ↔ surface) prevents in-mod aliasing in
the lib-test target (form-a.1 T5 precedent); preserved load_one
production-symbol-deletion by adding a test-mod-private load_one helper
in core's mod tests for the 2 pd.1-introduced tests still consuming it.

cargo test --workspace 573 green (+9 net from pd.1 baseline).
bench/cross_lang.py + bench/compile_check.py exit 0; bench/check.py
exit 1 with established noise envelope (bench_list_sum.bump_s, 13th
consecutive observation; pd.2 is workspace-loader-only, no codegen
touch — baseline pristine).
2026-05-14 12:57:27 +02:00

111 lines
4.5 KiB
Rust

//! 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_modules_with`]
//! (DFS-only loader) composed with [`ailang_core::workspace::build_workspace`]
//! (validation + registry). Surface owns the prelude inject step
//! between the two; the implicit-imports list `&["prelude"]` is
//! supplied at the surface call site. Introduced in iter pd.2 (post-
//! ext-cli.1's `load_workspace_with` shim retirement).
use ailang_core::ast::Module;
use ailang_core::workspace::{Workspace, WorkspaceLoadError};
use std::path::Path;
/// pd.2 (`prelude-decouple` milestone): the prelude bytes are embedded
/// here at compile time. The single source of truth is
/// `examples/prelude.ail` (Form A). Pre-pd.2, the embed lived in
/// `ailang-core` and used `prelude.ail.json`; that path retired with
/// the loader-split.
pub const PRELUDE_AIL: &str = include_str!("../../../examples/prelude.ail");
/// pd.2: parse the embedded prelude bytes into a `Module`.
///
/// Panics on parse failure — the prelude is build-time-validated by
/// every test run, so a parse failure here is a build-correctness
/// bug, not a runtime concern.
///
/// Mirrors the semantics of the retired `ailang_core::workspace::load_prelude`
/// (which deserialised `prelude.ail.json` via `serde_json::from_str`);
/// equivalence between the two parse paths is pinned by
/// `crates/ailang-surface/tests/prelude_module_hash_pin.rs`'s
/// cross-form-identity preflight.
pub fn parse_prelude() -> Module {
crate::parse(PRELUDE_AIL).expect("examples/prelude.ail must parse as a Module")
}
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> {
let (entry_name, root_dir, mut modules) =
ailang_core::workspace::load_modules_with(entry, load_module)?;
if modules.contains_key("prelude") {
return Err(WorkspaceLoadError::ReservedModuleName {
name: "prelude".to_string(),
});
}
modules.insert("prelude".to_string(), parse_prelude());
ailang_core::workspace::build_workspace(
entry_name,
root_dir,
modules,
&["prelude"],
)
}