use crate::util::resolve_model_path;
use run::arch::decoder::config::LlamaConfig;
use run::format::LoadedModel;
use run::ir::{family_graph, hex_encode, serialize};
pub fn run(args: Vec<String>) {
if args.is_empty() {
eprintln!("usage: mr graph <model> [--verify]");
std::process::exit(2);
}
let model_arg = &args[0];
let verify = args.iter().any(|a| a == "--verify");
let path = resolve_model_path(model_arg);
if !path.exists() {
eprintln!("model not found: {}", path.display());
std::process::exit(1);
}
println!("Loading: {}", path.display());
let lm = LoadedModel::load(&path).unwrap_or_else(|e| {
eprintln!("load failed: {e}");
std::process::exit(1);
});
let llama_cfg = LlamaConfig::parse(&lm.file.config, &lm.tensors).unwrap_or_else(|e| {
eprintln!("config parse failed: {e}");
eprintln!("note: only LlamaStyle families are supported currently");
std::process::exit(1);
});
let graph = match family_graph(&llama_cfg) {
Some(g) => g,
None => {
eprintln!(
"no template for model_type '{}' โ skipping (MoE/DiT/Whisper/BERT not yet ported)",
llama_cfg.model_type
);
std::process::exit(0);
}
};
let node_count = graph.len();
let bytes = serialize(&graph);
let hex = hex_encode(&bytes);
println!(
"Graph: {} nodes, {} bytes binary, {} bytes hex",
node_count,
bytes.len(),
hex.len()
);
let file_bytes = std::fs::read(&path).unwrap_or_else(|e| {
eprintln!("read failed: {e}");
std::process::exit(1);
});
let weights_marker = b"~~~weights\n";
let weights_data_start = file_bytes
.windows(weights_marker.len())
.position(|w| w == weights_marker)
.map(|p| p + weights_marker.len())
.unwrap_or(file_bytes.len());
let text_header_bytes = &file_bytes[..weights_data_start];
let binary_weights = &file_bytes[weights_data_start..];
let text_header = std::str::from_utf8(text_header_bytes).unwrap_or_else(|e| {
eprintln!("model text header is not valid UTF-8: {e}");
std::process::exit(1);
});
let new_text = inject_graph_section(text_header, &hex);
let mut new_content = new_text.into_bytes();
new_content.extend_from_slice(binary_weights);
std::fs::write(&path, &new_content).unwrap_or_else(|e| {
eprintln!("write failed: {e}");
std::process::exit(1);
});
println!("Written: {}", path.display());
if verify {
let lm2 = LoadedModel::load(&path).unwrap_or_else(|e| {
eprintln!("verify load failed: {e}");
std::process::exit(1);
});
match lm2.graph() {
Some(g) => println!("Verified: graph loads back with {} nodes โ", g.len()),
None => {
eprintln!("verify failed: graph section missing after write");
std::process::exit(1);
}
}
}
}
fn inject_graph_section(content: &str, hex: &str) -> String {
let graph_section = format!("~~~graph\n{hex}\n");
if let Some(start) = find_section_start(content, "~~~graph") {
let end = find_next_section(content, start + 1).unwrap_or(content.len());
let mut out = String::with_capacity(content.len() + graph_section.len());
out.push_str(&content[..start]);
out.push_str(&graph_section);
out.push_str(&content[end..]);
return out;
}
let insert_after = find_section_start(content, "~~~program")
.or_else(|| find_section_start(content, "~~~config"));
let insert_pos = if let Some(section_start) = insert_after {
find_next_section(content, section_start + 1).unwrap_or_else(|| {
find_section_start(content, "~~~weights").unwrap_or(content.len())
})
} else {
find_section_start(content, "~~~tensors")
.or_else(|| find_section_start(content, "~~~weights"))
.unwrap_or(content.len())
};
let mut out = String::with_capacity(content.len() + graph_section.len());
out.push_str(&content[..insert_pos]);
out.push_str(&graph_section);
out.push_str(&content[insert_pos..]);
out
}
fn find_section_start(content: &str, marker: &str) -> Option<usize> {
let mut pos = 0;
for line in content.lines() {
if line.trim_start().starts_with(marker) {
return Some(pos);
}
pos += line.len() + 1; }
None
}
fn find_next_section(content: &str, start: usize) -> Option<usize> {
let rest = &content[start..];
let after_first_newline = rest.find('\n').map(|i| i + 1).unwrap_or(rest.len());
let rest2 = &rest[after_first_newline..];
let mut pos = start + after_first_newline;
for line in rest2.lines() {
if line.trim_start().starts_with("~~~") {
return Some(pos);
}
pos += line.len() + 1;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inject_adds_graph_section() {
let content = "frontmatter\n~~~config\nkey = \"val\"\n~~~tensors\n[\"a\"]\n~~~weights\n";
let result = inject_graph_section(content, "deadbeef");
assert!(result.contains("~~~graph\ndeadbeef\n"));
let g_pos = result.find("~~~graph").unwrap();
let t_pos = result.find("~~~tensors").unwrap();
let w_pos = result.find("~~~weights").unwrap();
assert!(g_pos < t_pos);
assert!(g_pos < w_pos);
}
#[test]
fn inject_replaces_existing_graph_section() {
let content = "front\n~~~config\ncfg\n~~~graph\noldhex\n~~~tensors\nt\n~~~weights\n";
let result = inject_graph_section(content, "newhex");
assert!(result.contains("~~~graph\nnewhex\n"));
assert!(!result.contains("oldhex"));
}
#[test]
fn inject_inserts_after_program_section() {
let content = "front\n~~~config\ncfg\n~~~program\ncode\n~~~tensors\nt\n~~~weights\n";
let result = inject_graph_section(content, "graphhex");
let prog_end = result.find("~~~tensors").unwrap();
let g_pos = result.find("~~~graph").unwrap();
assert!(g_pos < prog_end);
}
}