1use std::collections::BTreeSet;
2use std::fmt;
3use std::ops::Range;
4
5use quote::ToTokens;
6use serde::{Deserialize, Serialize};
7use syn::spanned::Spanned;
8use syn::visit::{self, Visit};
9
10mod naming;
11pub mod plan;
12pub mod reassemble;
13pub use plan::{Oversized, Part, SplitPlan, plan_split};
14pub use reassemble::{OutputFile, SplitOutput, Topology, reassemble_bin, split_bin, split_mod};
15
16pub type Result<T> = std::result::Result<T, Error>;
17
18#[derive(Debug)]
19pub enum Error {
20 Parse(syn::Error),
21 NonCharBoundary { offset: usize },
22}
23
24impl fmt::Display for Error {
25 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 Self::Parse(error) => write!(formatter, "parse error: {error}"),
28 Self::NonCharBoundary { offset } => {
29 write!(formatter, "span offset {offset} is not a UTF-8 boundary")
30 }
31 }
32 }
33}
34
35impl std::error::Error for Error {}
36
37impl From<syn::Error> for Error {
38 fn from(error: syn::Error) -> Self {
39 Self::Parse(error)
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Exploded {
45 pub chunks: Vec<Chunk>,
46 pub manifest: Manifest,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Chunk {
51 pub byte_range: ByteRange,
52 pub text: String,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56pub struct ByteRange {
57 pub start: usize,
58 pub end: usize,
59}
60
61impl From<Range<usize>> for ByteRange {
62 fn from(range: Range<usize>) -> Self {
63 Self {
64 start: range.start,
65 end: range.end,
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71pub struct LineSpan {
72 pub start: usize,
73 pub end: usize,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct Manifest {
78 pub rows: Vec<ManifestRow>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82pub struct ManifestRow {
83 pub idx: usize,
84 pub chunk_index: usize,
85 pub name: String,
86 pub kind: String,
87 pub byte_range: ByteRange,
88 pub span: ByteRange,
89 pub line_span: LineSpan,
90 pub loc: usize,
91 pub adjacency_hint: Vec<String>,
92}
93
94pub fn explode(src: &str) -> Result<Exploded> {
95 let file = syn::parse_file(src)?;
96 let mut items = Vec::with_capacity(file.items.len());
97 for item in &file.items {
98 let range = checked_range(src, item.span().byte_range())?;
99 items.push(ItemInfo {
100 range,
101 name: item_name(item),
102 kind: item_kind(item).to_owned(),
103 adjacency_candidates: item_adjacency_candidates(item),
104 });
105 }
106
107 if items.is_empty() {
108 if src.is_empty() {
109 return Ok(Exploded {
110 chunks: Vec::new(),
111 manifest: Manifest { rows: Vec::new() },
112 });
113 }
114 return Ok(single_preamble(src));
115 }
116
117 let sibling_names = items
118 .iter()
119 .map(|item| item.name.clone())
120 .collect::<BTreeSet<_>>();
121
122 let mut chunk_ranges = Vec::with_capacity(items.len() + 1);
123 let first_start = if has_inner_file_attr(&src[..items[0].range.start]) {
124 if items[0].range.start > 0 {
125 chunk_ranges.push(ChunkSeed {
126 kind: ChunkSeedKind::Preamble,
127 range: 0..items[0].range.start,
128 });
129 }
130 items[0].range.start
131 } else {
132 0
133 };
134
135 for index in 0..items.len() {
136 let start = if index == 0 {
137 first_start
138 } else {
139 split_gap(src, items[index - 1].range.end, items[index].range.start)
140 };
141 let end = if index + 1 == items.len() {
142 src.len()
143 } else {
144 split_gap(src, items[index].range.end, items[index + 1].range.start)
145 };
146 chunk_ranges.push(ChunkSeed {
147 kind: ChunkSeedKind::Item(index),
148 range: start..end,
149 });
150 }
151
152 let mut chunks = Vec::with_capacity(chunk_ranges.len());
153 let mut rows = Vec::with_capacity(chunk_ranges.len());
154 for seed in chunk_ranges {
155 let chunk_index = chunks.len();
156 let text = src[seed.range.clone()].to_owned();
157 chunks.push(Chunk {
158 byte_range: seed.range.clone().into(),
159 text,
160 });
161
162 match seed.kind {
163 ChunkSeedKind::Preamble => rows.push(preamble_row(src, chunk_index, seed.range)),
164 ChunkSeedKind::Item(item_index) => {
165 let item = &items[item_index];
166 let adjacency_hint = item
167 .adjacency_candidates
168 .iter()
169 .filter(|name| sibling_names.contains(*name) && *name != &item.name)
170 .cloned()
171 .collect::<BTreeSet<_>>()
172 .into_iter()
173 .collect::<Vec<_>>();
174 rows.push(ManifestRow {
175 idx: rows.len(),
176 chunk_index,
177 name: item.name.clone(),
178 kind: item.kind.clone(),
179 byte_range: seed.range.into(),
180 span: item.range.clone().into(),
181 line_span: line_span(src, &item.range),
182 loc: loc(&chunks[chunk_index].text),
183 adjacency_hint,
184 });
185 }
186 }
187 }
188
189 Ok(Exploded {
190 chunks,
191 manifest: Manifest { rows },
192 })
193}
194
195pub fn manifest_toml(manifest: &Manifest) -> Result<String> {
196 toml::to_string_pretty(manifest).map_err(|error| {
197 Error::Parse(syn::Error::new(
198 proc_macro2::Span::call_site(),
199 format!("manifest serialization failed: {error}"),
200 ))
201 })
202}
203
204#[derive(Debug)]
205struct ItemInfo {
206 range: Range<usize>,
207 name: String,
208 kind: String,
209 adjacency_candidates: BTreeSet<String>,
210}
211
212#[derive(Debug)]
213struct ChunkSeed {
214 kind: ChunkSeedKind,
215 range: Range<usize>,
216}
217
218#[derive(Debug)]
219enum ChunkSeedKind {
220 Preamble,
221 Item(usize),
222}
223
224fn single_preamble(src: &str) -> Exploded {
225 let chunk = Chunk {
226 byte_range: ByteRange {
227 start: 0,
228 end: src.len(),
229 },
230 text: src.to_owned(),
231 };
232 Exploded {
233 chunks: vec![chunk],
234 manifest: Manifest {
235 rows: vec![preamble_row(src, 0, 0..src.len())],
236 },
237 }
238}
239
240fn preamble_row(src: &str, chunk_index: usize, range: Range<usize>) -> ManifestRow {
241 ManifestRow {
242 idx: chunk_index,
243 chunk_index,
244 name: "__preamble".to_owned(),
245 kind: "preamble".to_owned(),
246 byte_range: range.clone().into(),
247 span: range.clone().into(),
248 line_span: line_span(src, &range),
249 loc: loc(&src[range]),
250 adjacency_hint: Vec::new(),
251 }
252}
253
254fn checked_range(src: &str, range: Range<usize>) -> Result<Range<usize>> {
255 if !src.is_char_boundary(range.start) {
256 return Err(Error::NonCharBoundary {
257 offset: range.start,
258 });
259 }
260 if !src.is_char_boundary(range.end) {
261 return Err(Error::NonCharBoundary { offset: range.end });
262 }
263 Ok(range)
264}
265
266fn has_inner_file_attr(gap: &str) -> bool {
267 gap.lines().any(|line| {
268 let line = line.trim_start();
269 line.starts_with("//!") || line.starts_with("#![")
270 })
271}
272
273fn split_gap(src: &str, start: usize, end: usize) -> usize {
280 let gap = &src[start..end];
281 if gap.trim().is_empty() {
282 return end;
283 }
284
285 let mut lines = gap.split_inclusive('\n');
286 let mut offset = match lines.next() {
288 Some(tail) if tail.ends_with('\n') => tail.len(),
289 _ => return start,
290 };
291 let after_tail = offset;
292 let mut cut = None;
293 for line in lines {
294 if line.trim().is_empty() {
295 offset += line.len();
296 cut = Some(offset);
297 } else if cut.is_some() {
298 break;
299 } else {
300 offset += line.len();
301 }
302 }
303 start + cut.unwrap_or(after_tail)
304}
305
306fn line_span(src: &str, range: &Range<usize>) -> LineSpan {
307 if src.is_empty() {
308 return LineSpan { start: 1, end: 1 };
309 }
310 let start = byte_to_line(src, range.start);
311 let end_offset = if range.end > range.start {
312 range.end.saturating_sub(1)
313 } else {
314 range.end
315 };
316 let end = byte_to_line(src, end_offset);
317 LineSpan { start, end }
318}
319
320fn byte_to_line(src: &str, offset: usize) -> usize {
321 src[..offset.min(src.len())]
322 .bytes()
323 .filter(|byte| *byte == b'\n')
324 .count()
325 + 1
326}
327
328fn loc(text: &str) -> usize {
329 if text.is_empty() {
330 0
331 } else {
332 text.lines().count().max(1)
333 }
334}
335
336fn item_kind(item: &syn::Item) -> &'static str {
337 match item {
338 syn::Item::Const(_) => "const",
339 syn::Item::Enum(_) => "enum",
340 syn::Item::ExternCrate(_) => "extern_crate",
341 syn::Item::Fn(_) => "fn",
342 syn::Item::ForeignMod(_) => "foreign_mod",
343 syn::Item::Impl(_) => "impl",
344 syn::Item::Macro(item) if is_macro_rules(item) => "macro_rules",
345 syn::Item::Macro(_) => "macro",
346 syn::Item::Mod(_) => "mod",
347 syn::Item::Static(_) => "static",
348 syn::Item::Struct(_) => "struct",
349 syn::Item::Trait(_) => "trait",
350 syn::Item::TraitAlias(_) => "trait_alias",
351 syn::Item::Type(_) => "type",
352 syn::Item::Union(_) => "union",
353 syn::Item::Use(_) => "use",
354 syn::Item::Verbatim(_) => "verbatim",
355 _ => "unknown",
356 }
357}
358
359fn item_name(item: &syn::Item) -> String {
360 match item {
361 syn::Item::Const(item) => item.ident.to_string(),
362 syn::Item::Enum(item) => item.ident.to_string(),
363 syn::Item::ExternCrate(item) => item.ident.to_string(),
364 syn::Item::Fn(item) => item.sig.ident.to_string(),
365 syn::Item::ForeignMod(item) => {
366 let name = item.abi.name.as_ref().map(|name| name.value());
367 match name {
368 Some(name) => format!("extern \"{name}\""),
369 None => "extern".to_owned(),
370 }
371 }
372 syn::Item::Impl(item) => impl_name(item),
373 syn::Item::Macro(item) => macro_name(item),
374 syn::Item::Mod(item) => item.ident.to_string(),
375 syn::Item::Static(item) => item.ident.to_string(),
376 syn::Item::Struct(item) => item.ident.to_string(),
377 syn::Item::Trait(item) => item.ident.to_string(),
378 syn::Item::TraitAlias(item) => item.ident.to_string(),
379 syn::Item::Type(item) => item.ident.to_string(),
380 syn::Item::Union(item) => item.ident.to_string(),
381 syn::Item::Use(item) => use_tree_name(&item.tree),
382 syn::Item::Verbatim(tokens) => tokens.to_string(),
383 _ => "unknown".to_owned(),
384 }
385}
386
387fn impl_name(item: &syn::ItemImpl) -> String {
388 let self_ty = normalize_tokens(&item.self_ty.to_token_stream().to_string());
389 if let Some((_, path, _)) = &item.trait_ {
390 let trait_name = normalize_tokens(&path.to_token_stream().to_string());
391 format!("impl {trait_name} for {self_ty}")
392 } else {
393 format!("impl {self_ty}")
394 }
395}
396
397fn macro_name(item: &syn::ItemMacro) -> String {
398 item.ident
399 .as_ref()
400 .map(ToString::to_string)
401 .unwrap_or_else(|| normalize_tokens(&item.mac.path.to_token_stream().to_string()))
402}
403
404fn is_macro_rules(item: &syn::ItemMacro) -> bool {
405 normalize_tokens(&item.mac.path.to_token_stream().to_string()) == "macro_rules"
406}
407
408fn use_tree_name(tree: &syn::UseTree) -> String {
409 match tree {
410 syn::UseTree::Path(path) => format!("{}::{}", path.ident, use_tree_name(&path.tree)),
411 syn::UseTree::Name(name) => name.ident.to_string(),
412 syn::UseTree::Rename(rename) => format!("{} as {}", rename.ident, rename.rename),
413 syn::UseTree::Glob(_) => "*".to_owned(),
414 syn::UseTree::Group(group) => {
415 let names = group
416 .items
417 .iter()
418 .map(use_tree_name)
419 .collect::<Vec<_>>()
420 .join(", ");
421 format!("{{ {names} }}")
422 }
423 }
424}
425
426fn normalize_tokens(value: &str) -> String {
427 value
428 .replace(" :: ", "::")
429 .replace(" < ", "<")
430 .replace(" >", ">")
431 .replace(" ,", ",")
432 .replace("( ", "(")
433 .replace(" )", ")")
434 .replace("[ ", "[")
435 .replace(" ]", "]")
436}
437
438fn item_adjacency_candidates(item: &syn::Item) -> BTreeSet<String> {
439 let mut visitor = AdjacencyVisitor::default();
440 visitor.visit_item(item);
441 visitor.names
442}
443
444pub(crate) fn referenced_idents(chunk_text: &str) -> Option<BTreeSet<String>> {
449 let item = syn::parse_str::<syn::Item>(chunk_text).ok()?;
450 let mut visitor = DependencyVisitor::default();
451 visitor.visit_item(&item);
452 Some(visitor.names)
453}
454
455pub(crate) fn referenced_idents_in_source(src: &str) -> Option<BTreeSet<String>> {
460 let file = syn::parse_str::<syn::File>(src).ok()?;
461 let mut visitor = DependencyVisitor::default();
462 for item in &file.items {
463 visitor.visit_item(item);
464 }
465 Some(visitor.names)
466}
467
468#[derive(Default)]
472struct DependencyVisitor {
473 names: BTreeSet<String>,
474}
475
476impl<'ast> Visit<'ast> for DependencyVisitor {
477 fn visit_ident(&mut self, ident: &'ast proc_macro2::Ident) {
478 self.names.insert(ident.to_string());
479 }
480
481 fn visit_macro(&mut self, mac: &'ast syn::Macro) {
482 collect_macro_idents(&mac.tokens, &mut self.names);
483 visit::visit_macro(self, mac);
484 }
485
486 fn visit_attribute(&mut self, attr: &'ast syn::Attribute) {
487 collect_macro_idents(&attr.meta.to_token_stream(), &mut self.names);
488 visit::visit_attribute(self, attr);
489 }
490}
491
492fn collect_macro_idents(tokens: &proc_macro2::TokenStream, names: &mut BTreeSet<String>) {
494 for tree in tokens.clone() {
495 match tree {
496 proc_macro2::TokenTree::Ident(ident) => {
497 names.insert(ident.to_string());
498 }
499 proc_macro2::TokenTree::Group(group) => collect_macro_idents(&group.stream(), names),
500 _ => {}
501 }
502 }
503}
504
505#[derive(Default)]
506struct AdjacencyVisitor {
507 names: BTreeSet<String>,
508}
509
510impl<'ast> Visit<'ast> for AdjacencyVisitor {
511 fn visit_ident(&mut self, ident: &'ast proc_macro2::Ident) {
512 self.names.insert(ident.to_string());
513 }
514
515 fn visit_item_fn(&mut self, item: &'ast syn::ItemFn) {
516 for input in &item.sig.inputs {
517 self.visit_fn_arg(input);
518 }
519 self.visit_block(&item.block);
520 }
521
522 fn visit_item_const(&mut self, item: &'ast syn::ItemConst) {
523 self.visit_type(&item.ty);
524 self.visit_expr(&item.expr);
525 }
526
527 fn visit_item_static(&mut self, item: &'ast syn::ItemStatic) {
528 self.visit_type(&item.ty);
529 self.visit_expr(&item.expr);
530 }
531
532 fn visit_item_type(&mut self, item: &'ast syn::ItemType) {
533 self.visit_type(&item.ty);
534 }
535
536 fn visit_item_struct(&mut self, item: &'ast syn::ItemStruct) {
537 self.visit_fields(&item.fields);
538 }
539
540 fn visit_item_enum(&mut self, item: &'ast syn::ItemEnum) {
541 for variant in &item.variants {
542 self.visit_fields(&variant.fields);
543 if let Some((_, discriminant)) = &variant.discriminant {
544 self.visit_expr(discriminant);
545 }
546 }
547 }
548
549 fn visit_item_union(&mut self, item: &'ast syn::ItemUnion) {
550 self.visit_fields_named(&item.fields);
551 }
552
553 fn visit_item_trait(&mut self, item: &'ast syn::ItemTrait) {
554 for supertrait in &item.supertraits {
555 self.visit_type_param_bound(supertrait);
556 }
557 for trait_item in &item.items {
558 self.visit_trait_item(trait_item);
559 }
560 }
561
562 fn visit_item_trait_alias(&mut self, item: &'ast syn::ItemTraitAlias) {
563 for bound in &item.bounds {
564 self.visit_type_param_bound(bound);
565 }
566 }
567
568 fn visit_item_impl(&mut self, item: &'ast syn::ItemImpl) {
569 self.visit_type(&item.self_ty);
570 if let Some((_, path, _)) = &item.trait_ {
571 self.visit_path(path);
572 }
573 for impl_item in &item.items {
574 self.visit_impl_item(impl_item);
575 }
576 }
577
578 fn visit_item_macro(&mut self, item: &'ast syn::ItemMacro) {
579 self.visit_macro(&item.mac);
580 }
581
582 fn visit_macro(&mut self, mac: &'ast syn::Macro) {
583 collect_macro_idents(&mac.tokens, &mut self.names);
588 visit::visit_macro(self, mac);
589 }
590
591 fn visit_item_mod(&mut self, item: &'ast syn::ItemMod) {
592 if let Some((_, items)) = &item.content {
593 for item in items {
594 self.visit_item(item);
595 }
596 }
597 }
598
599 fn visit_pat_ident(&mut self, pat: &'ast syn::PatIdent) {
600 self.names.insert(pat.ident.to_string());
601 visit::visit_pat_ident(self, pat);
602 }
603}