use std::fs;
use std::path::PathBuf;
use aristo_core::walk::{
extract_c_expose_directives, extract_c_inspect_directives, CExposeDirective, CInspectDirective,
};
use super::{write_file, WriteOutcome, ARISTO_ABI};
use crate::{CliError, CliResult};
fn render_header(
inspects: &[CInspectDirective],
exposes: &[CExposeDirective],
handle_header: &str,
) -> String {
let mut s = String::new();
s.push_str("/* aristo_generated.h — generated by `aristo instrument gen-c`. Do not edit. */\n");
s.push_str("#ifndef ARISTO_GENERATED_H\n#define ARISTO_GENERATED_H\n\n");
s.push_str("#ifdef ARISTO_INSTRUMENT\n\n");
s.push_str("#include \"aristo.h\"\n");
s.push_str(&format!("#include \"{handle_header}\"\n\n"));
s.push_str(&abi_pin());
s.push('\n');
for d in inspects {
s.push_str(&format!(
"{} {}(const {} *self);\n",
d.ret,
d.accessor_name(),
d.type_name
));
}
for e in exposes {
s.push_str(&format!("{};\n", e.signature));
}
s.push_str("\n#endif /* ARISTO_INSTRUMENT */\n");
s.push_str("#endif /* ARISTO_GENERATED_H */\n");
s
}
fn render_source(directives: &[CInspectDirective]) -> String {
let mut s = String::new();
s.push_str("/* aristo_generated.c — generated by `aristo instrument gen-c`. Do not edit. */\n");
s.push_str("/* #include this into the .c where the annotated struct is fully defined. */\n");
s.push_str("#ifdef ARISTO_INSTRUMENT\n\n");
s.push_str("#include \"aristo.h\"\n\n");
s.push_str(&abi_pin());
s.push('\n');
for d in directives {
let body = match &d.with {
Some(with) => format!("return {with}(&self->{});", d.field),
None => format!("return self->{};", d.field),
};
s.push_str(&format!(
"{} {}(const {} *self) {{ {body} }}\n",
d.ret,
d.accessor_name(),
d.type_name
));
}
s.push_str("\n#endif /* ARISTO_INSTRUMENT */\n");
s
}
fn abi_pin() -> String {
format!(
"_Static_assert(ARISTO_ABI == {ARISTO_ABI},\n \"aristo.h ABI does not match the \
generator that produced this file; re-vendor and re-run gen-c \
from the same aristo CLI version\");\n"
)
}
fn collect_directives(
paths: &[PathBuf],
) -> CliResult<(Vec<CInspectDirective>, Vec<CExposeDirective>)> {
let mut inspects = Vec::new();
let mut exposes = Vec::new();
for p in paths {
let src = fs::read_to_string(p)?;
let to_err = |e| CliError::Other {
message: format!("failed to parse {}: {e}", p.display()),
exit_code: 2,
};
inspects.extend(extract_c_inspect_directives(&src).map_err(to_err)?);
exposes.extend(extract_c_expose_directives(&src).map_err(to_err)?);
}
Ok((inspects, exposes))
}
pub(crate) fn run(
paths: Vec<PathBuf>,
handle_header: String,
out: PathBuf,
check: bool,
) -> CliResult<()> {
let (inspects, exposes) = collect_directives(&paths)?;
let header = render_header(&inspects, &exposes, &handle_header);
let source = render_source(&inspects);
let files = [
("aristo_generated.h", header),
("aristo_generated.c", source),
];
if check {
let mut drifted = Vec::new();
for (name, content) in &files {
let path = out.join(name);
let current = fs::read_to_string(&path).unwrap_or_default();
if current != *content {
drifted.push(name.to_string());
}
}
if drifted.is_empty() {
println!(
"ok: generated C instrumentation is up to date ({} accessor(s), {} exposed fn(s)).",
inspects.len(),
exposes.len()
);
return Ok(());
}
return Err(CliError::Other {
message: format!(
"generated C instrumentation is stale: {} differ from the directives.\n \
Re-run `aristo instrument gen-c` and commit the result.",
drifted.join(", ")
),
exit_code: 2,
});
}
println!(
"→ Generating {} accessor(s) + {} exposed fn(s) to {}/ …",
inspects.len(),
exposes.len(),
out.display()
);
for (name, content) in &files {
let outcome = write_file(&out.join(name), content)?;
let verb = match outcome {
WriteOutcome::Created => "created",
WriteOutcome::Updated => "updated",
WriteOutcome::Unchanged => "unchanged",
};
println!(" • {name} {verb}");
}
println!();
println!(
"ok: C instrumentation generated ({} accessor(s), {} exposed fn(s)).",
inspects.len(),
exposes.len()
);
println!(" `#include \"aristo_generated.c\"` (gated) into the .c that defines the struct;");
println!(" the harness includes `aristo_generated.h`.");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn dir(type_name: &str, field: &str, ret: &str, with: Option<&str>) -> CInspectDirective {
CInspectDirective {
type_name: type_name.into(),
field: field.into(),
ret: ret.into(),
with: with.map(str::to_string),
name: None,
line: 1,
}
}
#[test]
fn renders_clone_accessor_prototype_and_body() {
let ds = vec![dir("Db", "next_seqno", "uint64_t", None)];
let h = render_header(&ds, &[], "bitcask.h");
let c = render_source(&ds);
assert!(h.contains("uint64_t aristo_inspect_Db_next_seqno(const Db *self);"));
assert!(h.contains("#include \"bitcask.h\""));
assert!(c.contains(
"uint64_t aristo_inspect_Db_next_seqno(const Db *self) { return self->next_seqno; }"
));
assert!(h.contains("#ifdef ARISTO_INSTRUMENT"));
assert!(c.contains("#ifdef ARISTO_INSTRUMENT"));
assert!(h.contains(&format!("ARISTO_ABI == {ARISTO_ABI}")));
assert!(c.contains(&format!("ARISTO_ABI == {ARISTO_ABI}")));
}
#[test]
fn renders_projection_body_through_the_projector() {
let ds = vec![dir("Db", "keydir", "size_t", Some("keydir_live_count"))];
let c = render_source(&ds);
assert!(c.contains(
"size_t aristo_inspect_Db_keydir(const Db *self) { return keydir_live_count(&self->keydir); }"
));
}
#[test]
fn accessors_render_in_directive_order() {
let ds = vec![dir("Db", "a", "int", None), dir("Db", "b", "long", None)];
let c = render_source(&ds);
let ia = c.find("aristo_inspect_Db_a").unwrap();
let ib = c.find("aristo_inspect_Db_b").unwrap();
assert!(ia < ib, "accessors must render in directive (source) order");
}
#[test]
fn empty_directives_still_render_a_valid_gated_pair() {
let h = render_header(&[], &[], "db.h");
let c = render_source(&[]);
assert!(h.contains("#ifndef ARISTO_GENERATED_H"));
assert!(h.contains("#ifdef ARISTO_INSTRUMENT"));
assert!(c.contains("#ifdef ARISTO_INSTRUMENT"));
}
#[test]
fn renders_exposed_function_prototype_verbatim() {
let exposes = vec![CExposeDirective {
name: "recover_replay".into(),
signature: "ARISTO_TU_LOCAL int recover_replay(Db *db)".into(),
line: 1,
}];
let h = render_header(&[], &exposes, "db.h");
assert!(h.contains("ARISTO_TU_LOCAL int recover_replay(Db *db);"));
let c = render_source(&[]);
assert!(!c.contains("recover_replay"));
}
#[test]
fn end_to_end_reads_a_c_fixture_and_writes_files() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("db.c");
fs::write(
&src,
"// @aristo inspect(field = \"next_seqno\", ret = \"uint64_t\")\n\
typedef struct { unsigned long next_seqno; } Db;\n",
)
.unwrap();
let out = tmp.path().join("aristo");
run(vec![src.clone()], "db.h".into(), out.clone(), false).unwrap();
let c = fs::read_to_string(out.join("aristo_generated.c")).unwrap();
assert!(
c.contains("aristo_inspect_Db_next_seqno(const Db *self) { return self->next_seqno; }")
);
run(vec![src.clone()], "db.h".into(), out.clone(), true).unwrap();
fs::write(
&src,
"// @aristo inspect(field = \"next_seqno\", ret = \"uint64_t\", name = \"seq\")\n\
typedef struct { unsigned long next_seqno; } Db;\n",
)
.unwrap();
let stale = run(vec![src], "db.h".into(), out, true);
assert!(
stale.is_err(),
"--check must fail when directives drift from generated files"
);
}
}