//! 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 { 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 `.ail` over /// `.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 { 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"], ) }