//! Surface-side loader: dispatches by file extension. //! //! `.ail` → `surface::parse` (Form A); `.ail.json` → `core::load_module` //! (Form B). Workspace import resolution prefers `.ail` over `.ail.json`. use ailang_surface::{load_module, load_workspace}; use std::fs; /// Property: `load_module` on a `.ail` (Form A) source file dispatches /// to `ailang_surface::parse` and returns the parsed module. Guards /// against the dispatch silently falling through to the JSON loader /// (which would fail on Form-A bytes with the misleading /// `json: expected value at line 1 column 1` error the rest of this /// iter exists to eliminate). #[test] fn load_module_dispatches_ail_to_surface_parse() { let tmp = tempfile::tempdir().expect("tempdir"); let path = tmp.path().join("hello.ail"); // Smallest valid Form-A module: header + no defs. fs::write(&path, "(module hello)\n").expect("write"); let module = load_module(&path).expect("surface parses .ail"); assert_eq!(module.name, "hello"); } /// Property: `load_module` on a `.ail.json` (Form B) source file /// delegates to `ailang_core::load_module` and produces the same /// `Module` value the core entry point would. Guards against the /// surface loader silently shadowing or rewriting the JSON path. #[test] fn load_module_dispatches_ail_json_to_core_loader() { let tmp = tempfile::tempdir().expect("tempdir"); let path = tmp.path().join("dummy.ail.json"); fs::write( &path, r#"{"schema":"ailang/v0","name":"dummy","imports":[],"defs":[]}"#, ) .expect("write"); let surface_loaded = load_module(&path).expect("surface dispatches .ail.json to core"); let core_loaded = ailang_core::load_module(&path).expect("core loads directly"); assert_eq!(surface_loaded.name, core_loaded.name); assert_eq!(surface_loaded.schema, core_loaded.schema); } /// Property: workspace import resolution accepts mixed extensions — /// a `.ail` entry module can import a sibling provided as `.ail.json`, /// and both modules end up in the loaded workspace. Guards against /// the `.ail`-first / `.ail.json`-fallback precedence rule in /// `workspace::visit` regressing back to JSON-only resolution. #[test] fn load_workspace_resolves_ail_imports_first_then_ail_json() { let tmp = tempfile::tempdir().expect("tempdir"); let entry = tmp.path().join("main.ail"); // Surface grammar uses one `(import )` clause per import. fs::write(&entry, "(module main (import leaf))\n").expect("write entry"); let leaf = tmp.path().join("leaf.ail.json"); fs::write( &leaf, r#"{"schema":"ailang/v0","name":"leaf","imports":[],"defs":[]}"#, ) .expect("write leaf"); let ws = load_workspace(&entry).expect("workspace loads with mixed extensions"); assert!(ws.modules.contains_key("main")); assert!(ws.modules.contains_key("leaf")); } /// Property: when BOTH `.ail` (Form A) and `.ail.json` /// (Form B) exist in the workspace root, the `.ail` sibling wins — /// the loaded module's content reflects Form A, not Form B. Pins the /// `surface_path.is_file()` precedence branch in /// `workspace::visit`'s import-path construction. Regression would /// look like the loader silently shadowing a freshly-edited `.ail` /// with a stale `.ail.json` snapshot of the same module name. #[test] fn load_workspace_prefers_ail_when_both_extensions_exist() { let tmp = tempfile::tempdir().expect("tempdir"); // Entry imports `leaf`. let entry = tmp.path().join("main.ail"); fs::write(&entry, "(module main (import leaf))\n").expect("write entry"); // Both leaf.ail and leaf.ail.json exist, with different in-source content. // The `.ail` carries a `(const winner ...)`; the `.ail.json` carries a // `(const loser ...)`. The loader must pick `winner`. let leaf_ail = tmp.path().join("leaf.ail"); fs::write( &leaf_ail, "(module leaf (const winner (type (con Int)) (body 1)))\n", ) .expect("write leaf.ail"); let leaf_json = tmp.path().join("leaf.ail.json"); fs::write( &leaf_json, r#"{"schema":"ailang/v0","name":"leaf","imports":[],"defs":[{"kind":"const","name":"loser","type":{"k":"con","name":"Int"},"value":{"t":"lit","lit":{"kind":"int","value":2}}}]}"#, ) .expect("write leaf.ail.json"); let ws = load_workspace(&entry).expect("workspace loads with both extensions present"); let leaf = &ws.modules["leaf"]; let names: Vec<&str> = leaf .defs .iter() .map(ailang_core::def_name) .collect(); assert!( names.contains(&"winner"), "expected `.ail` content (def `winner`); got defs: {names:?}" ); assert!( !names.contains(&"loser"), "must NOT have loaded `.ail.json` content (def `loser`); got defs: {names:?}" ); }