//! 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 { 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 { load_workspace_with(entry, load_module) }