use crate::{Exploded, SplitPlan, plan_split};
mod body_paths;
mod imports;
mod inline_mod;
mod visibility;
use body_paths::*;
use imports::*;
use inline_mod::*;
use visibility::*;
const PART_OVERHEAD: usize = 3;
pub fn split_bin(exploded: &Exploded, max_loc: usize, root_stem: &str) -> SplitOutput {
let header_loc: usize = exploded
.manifest
.rows
.iter()
.filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
.map(|r| r.loc)
.sum();
let item_budget = max_loc.saturating_sub(header_loc + PART_OVERHEAD).max(1);
let plan = plan_split(exploded, item_budget);
reassemble_bin(exploded, &plan, root_stem, max_loc)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputFile {
pub path: String,
pub contents: String,
pub loc: usize,
}
#[derive(Debug, Clone)]
pub struct SplitOutput {
pub files: Vec<OutputFile>,
pub still_oversized: Vec<String>,
}
impl SplitOutput {
pub fn max_loc(&self) -> usize {
self.files.iter().map(|f| f.loc).max().unwrap_or(0)
}
}
fn loc(text: &str) -> usize {
if text.is_empty() {
0
} else {
text.lines().count().max(1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Topology {
Bin,
Mod,
}
impl Topology {
fn sibling_vis(self) -> &'static str {
match self {
Topology::Bin => "pub(crate) ",
Topology::Mod => NESTED_SIBLING_VIS,
}
}
}
const NESTED_SIBLING_VIS: &str = "pub(super) ";
pub fn split_mod(exploded: &Exploded, max_loc: usize, root_stem: &str) -> SplitOutput {
let header_loc: usize = exploded
.manifest
.rows
.iter()
.filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
.map(|r| r.loc)
.sum();
let item_budget = max_loc.saturating_sub(header_loc + PART_OVERHEAD).max(1);
let plan = plan_split(exploded, item_budget);
reassemble(exploded, &plan, root_stem, Topology::Mod, max_loc)
}
pub fn reassemble_bin(
exploded: &Exploded,
plan: &SplitPlan,
root_stem: &str,
max_loc: usize,
) -> SplitOutput {
reassemble(exploded, plan, root_stem, Topology::Bin, max_loc)
}
fn reassemble(
exploded: &Exploded,
plan: &SplitPlan,
root_stem: &str,
topology: Topology,
max_loc: usize,
) -> SplitOutput {
let sibling = match topology {
Topology::Bin => "crate",
Topology::Mod => "super",
};
let part_rebase = match topology {
Topology::Bin => Rebase::CrateRoot,
Topology::Mod => Rebase::Nested,
};
let chunk_text = |i: usize| exploded.chunks[i].text.as_str();
let row = |i: usize| {
exploded
.manifest
.rows
.iter()
.find(|r| r.chunk_index == i)
.unwrap()
};
let sub_prefix = match topology {
Topology::Bin => String::new(),
Topology::Mod if matches!(root_stem, "mod" | "lib" | "main") => String::new(),
Topology::Mod => format!("{root_stem}/"),
};
let preamble: String = plan
.header
.iter()
.filter(|&&i| row(i).kind == "preamble")
.map(|&i| chunk_text(i))
.collect();
let import_chunks: Vec<usize> = plan
.header
.iter()
.copied()
.filter(|&i| row(i).kind != "preamble")
.collect();
let root_kept: String = plan.root_items.iter().map(|&i| chunk_text(i)).collect();
let mut reserved: std::collections::BTreeSet<String> =
plan.mods.iter().map(|&i| row(i).name.clone()).collect();
reserved.insert(root_stem.to_owned());
for &i in plan
.root_items
.iter()
.chain(&import_chunks)
.chain(plan.oversized.iter().map(|o| &o.chunk_index))
{
if row(i).kind == "mod" {
reserved.insert(row(i).name.clone());
} else if let Some(names) = import_provided_names(chunk_text(i)) {
reserved.extend(names);
} else if matches!(
row(i).kind.as_str(),
"struct" | "enum" | "union" | "type" | "trait" | "trait_alias"
) {
reserved.insert(row(i).name.clone());
}
}
let local_names = local_path_roots(exploded);
let mut files = Vec::new();
let mut mod_decls = String::new();
let mut reexports = String::new();
let mut still_oversized = Vec::new();
let mut root_refs = Refs::default();
for &i in &plan.root_items {
root_refs.add(chunk_text(i));
}
for part in &plan.parts {
let mut module = part.name.clone();
while !reserved.insert(module.clone()) {
module.push('_');
}
let mut body = String::new();
let mut root_main = None;
let mut export_vis = ExportVis::None;
let mut refs = Refs::default();
for &i in &part.chunk_indices {
let r = row(i);
if r.kind == "fn" && r.name == "main" {
root_main = Some(i);
continue;
}
export_vis = export_vis.max(item_export_vis(chunk_text(i)));
refs.add(chunk_text(i));
body.push_str(&rebase_body_paths(
&bump_visibility(chunk_text(i), topology.sibling_vis()),
part_rebase,
));
}
if body.trim().is_empty() {
if let Some(i) = root_main {
files.push(OutputFile {
path: String::from("__root_main__"),
contents: chunk_text(i).to_owned(),
loc: 0,
});
}
continue;
}
if let Some(i) = root_main {
files.push(OutputFile {
path: String::from("__root_main__"),
contents: chunk_text(i).to_owned(),
loc: 0,
});
}
let part_imports = select_imports(
&import_chunks,
&exploded.chunks,
&refs,
part_rebase,
&local_names,
);
let contents = part_file(&part_imports, sibling, &body);
mod_decls.push_str(&format!("mod {module};\n"));
if let Some(line) = reexport_line(export_vis, &module, topology) {
reexports.push_str(&line);
}
let l = loc(&contents);
files.push(OutputFile {
path: format!("{sub_prefix}{module}.rs"),
contents,
loc: l,
});
}
for &mod_index in &plan.mods {
let chunk = chunk_text(mod_index);
let name = row(mod_index).name.as_str();
let Some((decl, inner)) = extract_mod(chunk) else {
root_refs.add(chunk);
mod_decls.push_str(chunk);
continue;
};
mod_decls.push_str(&decl);
root_refs.add_source(&inner);
let inner_loc = loc(&inner);
if inner_loc < max_loc {
files.push(OutputFile {
path: format!("{sub_prefix}{name}.rs"),
contents: inner,
loc: inner_loc,
});
} else {
let (nested, nested_still) = split_nested_mod(name, &inner, max_loc);
files.extend(nested.into_iter().map(|mut f| {
f.path = format!("{sub_prefix}{}", f.path);
f
}));
still_oversized.extend(nested_still);
}
}
let mut oversized_kept = String::new();
for over in &plan.oversized {
let chunk = chunk_text(over.chunk_index);
oversized_kept.push_str(chunk);
root_refs.add(chunk);
still_oversized.push(format!(
"{} ({} LOC {}, manual extraction)",
over.name, over.loc, over.kind
));
}
let mut root_main = String::new();
files.retain(|f| {
if f.path == "__root_main__" {
root_main = f.contents.clone();
false
} else {
true
}
});
if !root_main.trim().is_empty() {
root_refs.add(&root_main);
}
let root_imports = if root_refs.is_empty() {
String::new()
} else {
select_imports(
&import_chunks,
&exploded.chunks,
&root_refs,
Rebase::Keep,
&local_names,
)
};
let mut root = String::new();
root.push_str(&preamble);
root.push_str(&root_imports);
root.push_str(&root_kept);
root.push('\n');
root.push_str(&mod_decls);
root.push('\n');
root.push_str(&reexports);
root.push_str(&oversized_kept);
if !root_main.is_empty() {
root.push('\n');
root.push_str(&root_main);
}
let root_loc = loc(&root);
files.insert(
0,
OutputFile {
path: format!("{root_stem}.rs"),
contents: root,
loc: root_loc,
},
);
for file in &mut files {
let levels = file.path.matches('/').count();
if levels == 0 || !file.contents.contains("include") {
continue;
}
file.contents = rebase_includes(&file.contents, levels, &file.path);
}
for file in &files {
if file.loc >= max_loc {
still_oversized.push(format!(
"{} ({} LOC, contents preserved; exceeds output budget)",
file.path, file.loc
));
}
}
SplitOutput {
files,
still_oversized,
}
}
fn part_file(imports: &str, sibling: &str, body: &str) -> String {
if imports.trim().is_empty() {
format!("use {sibling}::*;\n\n{body}")
} else {
format!("{imports}\nuse {sibling}::*;\n\n{body}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{explode, plan_split};
mod body_paths;
mod imports;
mod inline_mod;
mod split;
mod visibility;
}