refactor(cli): collapse duplicated handler helpers in main.rs

CLI contract (stdout/stderr/exit codes/JSON field order) unchanged —
the e2e diff/manifest/describe cases plus the staticlib/emit-ir/
roundtrip CLI tests are green.

- locate_bump_runtime / locate_rc_runtime / locate_str_runtime were
  three copies of the same upward runtime/<file> walk; folded into
  locate_runtime_file(filename, err_msg) with each error string passed
  through verbatim.
- describe built the same single-def Module wrapper in both branches;
  hoisted into wrap_single_def.
- manifest built the same per-symbol JSON object in two branches
  (workspace adds a leading "module" field); hoisted into
  manifest_def_json(def, Option<module>), field insertion order
  preserved (serde_json preserve_order).

Left duplicated by design: the build_to/build_staticlib elaborate-or-
exit blocks (a shared helper would have to name a MIR type from a crate
ail does not depend on) and the Diff handler's two-report branches (a
generic output router was out of scope and no cleaner).
This commit is contained in:
2026-06-02 02:24:34 +02:00
parent 10da23304e
commit 72503c40fe
+67 -83
View File
@@ -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<String>) {
}
}
/// 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 <name>`.
///
/// 1. `name` contains exactly one dot → resolve `<module>.<def>` strictly.
@@ -2146,31 +2155,10 @@ fn parse_alloc_strategy(s: &str) -> Result<ailang_codegen::AllocStrategy> {
/// `--alloc=bump` path is opt-in, so this only fires when the user
/// asked for it.
fn locate_bump_runtime() -> Result<PathBuf> {
// 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<PathBuf> {
/// `ailang_rc_alloc` / `ailang_rc_inc` / `ailang_rc_dec` against
/// libc malloc/free.
fn locate_rc_runtime() -> Result<PathBuf> {
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<PathBuf> {
/// header (codegen) without an alloc-strategy guard, so the symbol
/// must always be resolvable at link time.
fn locate_str_runtime() -> Result<PathBuf> {
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/<filename>` 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<PathBuf> {
let candidates = [
std::env::current_exe().ok(),
std::env::current_dir().ok(),
@@ -2218,7 +2205,7 @@ fn locate_str_runtime() -> Result<PathBuf> {
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<PathBuf> {
}
}
}
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