diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index db33386..9b6160f 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -365,17 +365,7 @@ fn main() -> Result<()> { if json { let symbols: Vec<_> = entries .iter() - .map(|(mod_name, d)| { - let (kind, ty, effects) = def_summary(d); - serde_json::json!({ - "module": mod_name, - "name": d.name(), - "kind": kind, - "type": ty, - "effects": effects, - "hash": ailang_core::def_hash(d), - }) - }) + .map(|(mod_name, d)| manifest_def_json(d, Some(mod_name))) .collect(); let out = serde_json::json!({ "workspace": ws.entry, @@ -414,16 +404,7 @@ fn main() -> Result<()> { let entries: Vec<_> = m .defs .iter() - .map(|d| { - let (kind, ty, effects) = def_summary(d); - serde_json::json!({ - "name": d.name(), - "kind": kind, - "type": ty, - "effects": effects, - "hash": ailang_core::def_hash(d), - }) - }) + .map(|d| manifest_def_json(d, None)) .collect(); let out = serde_json::json!({ "module": m.name, @@ -543,13 +524,7 @@ fn main() -> Result<()> { println!("{}", serde_json::to_string_pretty(&v)?); } else { let m = ws.modules.get(&mod_name).unwrap(); - let one = ailang_core::Module { - schema: m.schema.clone(), - name: m.name.clone(), - kernel: false, - imports: vec![], - defs: vec![def.clone()], - }; + let one = wrap_single_def(&m.schema, &m.name, def); let h = ailang_core::def_hash(def); println!("module: {}", mod_name); println!("hash: {h}"); @@ -567,13 +542,7 @@ fn main() -> Result<()> { println!("{s}"); } else { // Form-(A) projection of a one-def module. - let one = ailang_core::Module { - schema: m.schema.clone(), - name: m.name.clone(), - kernel: false, - imports: vec![], - defs: vec![def.clone()], - }; + let one = wrap_single_def(&m.schema, &m.name, def); let h = ailang_core::def_hash(def); println!("hash: {h}"); print!("{}", ailang_surface::print(&one)); @@ -1865,6 +1834,46 @@ fn def_summary(d: &ailang_core::Def) -> (&'static str, String, Vec) { } } +/// Build the per-symbol JSON object the `manifest` command emits for +/// one def. When `module` is `Some`, a leading `"module"` field is +/// inserted FIRST, matching the workspace branch; the single-module +/// branch passes `None` and the object starts at `"name"`. Field +/// insertion order is observable (serde_json `preserve_order`), so the +/// order here — `module?`, `name`, `kind`, `type`, `effects`, `hash` — +/// is the contract both branches relied on inline. +fn manifest_def_json(d: &ailang_core::Def, module: Option<&str>) -> serde_json::Value { + let (kind, ty, effects) = def_summary(d); + let mut obj = serde_json::Map::new(); + if let Some(m) = module { + obj.insert("module".to_string(), serde_json::json!(m)); + } + obj.insert("name".to_string(), serde_json::json!(d.name())); + obj.insert("kind".to_string(), serde_json::json!(kind)); + obj.insert("type".to_string(), serde_json::json!(ty)); + obj.insert("effects".to_string(), serde_json::json!(effects)); + obj.insert("hash".to_string(), serde_json::json!(ailang_core::def_hash(d))); + serde_json::Value::Object(obj) +} + +/// Wrap a single def in a one-def `Module` for Form-(A) projection. +/// Both `describe` branches (workspace and single-module) render a +/// lone def by building this exact wrapper — same schema/name from the +/// host module, `kernel: false`, no imports — then handing it to +/// `ailang_surface::print`. Shared so the projected text cannot drift. +fn wrap_single_def( + schema: &str, + name: &str, + def: &ailang_core::Def, +) -> ailang_core::Module { + ailang_core::Module { + schema: schema.to_string(), + name: name.to_string(), + kernel: false, + imports: vec![], + defs: vec![def.clone()], + } +} + /// Resolution for `ail describe --workspace `. /// /// 1. `name` contains exactly one dot → resolve `.` strictly. @@ -2146,31 +2155,10 @@ fn parse_alloc_strategy(s: &str) -> Result { /// `--alloc=bump` path is opt-in, so this only fires when the user /// asked for it. fn locate_bump_runtime() -> Result { - // Two anchors we try in order: - // 1. the directory containing the running `ail` binary, walked - // up to find `runtime/bump.c` (handles `target/release/ail` - // and `target/debug/ail` cleanly). - // 2. the current working directory, walked up. - let candidates = [ - std::env::current_exe().ok(), - std::env::current_dir().ok(), - ]; - for start in candidates.iter().flatten() { - let mut cur: &Path = start.as_path(); - loop { - let candidate = cur.join("runtime").join("bump.c"); - if candidate.exists() { - return Ok(candidate); - } - match cur.parent() { - Some(p) => cur = p, - None => break, - } - } - } - anyhow::bail!( + locate_runtime_file( + "bump.c", "could not locate `runtime/bump.c` (required for --alloc=bump). \ - Run `ail` from inside the AILang workspace." + Run `ail` from inside the AILang workspace.", ) } @@ -2180,26 +2168,10 @@ fn locate_bump_runtime() -> Result { /// `ailang_rc_alloc` / `ailang_rc_inc` / `ailang_rc_dec` against /// libc malloc/free. fn locate_rc_runtime() -> Result { - let candidates = [ - std::env::current_exe().ok(), - std::env::current_dir().ok(), - ]; - for start in candidates.iter().flatten() { - let mut cur: &Path = start.as_path(); - loop { - let candidate = cur.join("runtime").join("rc.c"); - if candidate.exists() { - return Ok(candidate); - } - match cur.parent() { - Some(p) => cur = p, - None => break, - } - } - } - anyhow::bail!( + locate_runtime_file( + "rc.c", "could not locate `runtime/rc.c` (required for --alloc=rc). \ - Run `ail` from inside the AILang workspace." + Run `ail` from inside the AILang workspace.", ) } @@ -2211,6 +2183,21 @@ fn locate_rc_runtime() -> Result { /// header (codegen) without an alloc-strategy guard, so the symbol /// must always be resolvable at link time. fn locate_str_runtime() -> Result { + locate_runtime_file( + "str.c", + "could not locate `runtime/str.c` (linked unconditionally). \ + Run `ail` from inside the AILang workspace.", + ) +} + +/// Shared upward walk for the `locate_*_runtime` helpers. Searches +/// for `runtime/` from two anchors, in order: +/// 1. the directory containing the running `ail` binary, walked up +/// (handles `target/release/ail` and `target/debug/ail`). +/// 2. the current working directory, walked up. +/// On miss, fails with `err_msg` verbatim — callers pass the +/// byte-identical message their flag expects. +fn locate_runtime_file(filename: &str, err_msg: &str) -> Result { let candidates = [ std::env::current_exe().ok(), std::env::current_dir().ok(), @@ -2218,7 +2205,7 @@ fn locate_str_runtime() -> Result { for start in candidates.iter().flatten() { let mut cur: &Path = start.as_path(); loop { - let candidate = cur.join("runtime").join("str.c"); + let candidate = cur.join("runtime").join(filename); if candidate.exists() { return Ok(candidate); } @@ -2228,10 +2215,7 @@ fn locate_str_runtime() -> Result { } } } - anyhow::bail!( - "could not locate `runtime/str.c` (linked unconditionally). \ - Run `ail` from inside the AILang workspace." - ) + anyhow::bail!("{}", err_msg) } /// print build-time diagnostics to stderr in the `ail check` human