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:
+67
-83
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user