Add `--prune`: translate only what the program reaches
A dependency graph carries far more code than any one program uses -- libcosmic declares 2,515 functions and the 356 crates behind it declare 176,686 items -- and because rustnim refuses rather than guesses, it was failing on constructs in code nothing calls. That answers a question nobody asked. With `--prune`, the first input is the program and the rest are libraries, and a library item is kept only if something reachable names it. Reachability is computed over names rather than types, which is deliberately coarse: it keeps more than is strictly needed, and that is the safe direction. Keeping too much costs translation effort. Keeping too little would drop a definition something calls -- and that surfaces as rustnim refusing an unknown function, loudly and at the call site, rather than as silently wrong output. Off by default: dropping code is not a thing to do silently. When on, it says how many items it dropped and how many it kept. tests/cases/065 is a library with an untranslatable function in it that the program never calls. Without `--prune` it fails on `std::process::id`; with it, three of six items are dropped and the program is byte-identical to rustc's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fefccb9 parent: 2cccbf7 modified
src/lower.rs +9 -0 | @@ -362,6 +362,14 @@ pub struct Lowerer { | ||
| 362 | 362 | /// the closure must have -- which is the only thing that types a closure |
| 363 | 363 | /// passed to a generic parameter. |
| 364 | 364 | fn_blankets: HashMap<String, (Vec<String>, Vec<Nim>, Nim)>, |
| 365 | + /// Procs that cannot be translated, by Nim name, with the reason. | |
| 366 | + /// | |
| 367 | + /// Not an error on its own. A dependency graph carries far more code than | |
| 368 | + /// any one program reaches -- 176,686 items behind libcosmic's 2,515 -- | |
| 369 | + /// and refusing to translate the whole graph because one unreached | |
| 370 | + /// function in it is untranslatable answers a question nobody asked. The | |
| 371 | + /// error belongs at the call site, and that is where it is raised. | |
| 372 | + untranslatable: HashMap<String, String>, | |
| 365 | 373 | /// Bounds discovered while rewriting `impl Trait` parameters. `rewrite_apit` |
| 366 | 374 | /// runs from `&self`, so they are parked here and folded into `bounds` by |
| 367 | 375 | /// whoever called it. |
| @@ -462,6 +470,7 @@ impl Lowerer { | ||
| 462 | 470 | opaque_why: None, |
| 463 | 471 | trait_methods: HashMap::new(), |
| 464 | 472 | fn_blankets: HashMap::new(), |
| 473 | + untranslatable: HashMap::new(), | |
| 465 | 474 | pending_apit_bounds: std::cell::RefCell::new(Vec::new()), |
| 466 | 475 | bounds: HashMap::new(), |
| 467 | 476 | const_names_at: std::cell::Cell::new(None), |
| @@ -362,6 +362,14 @@ pub struct Lowerer { | |||
| 362 | /// the closure must have -- which is the only thing that types a closure | 362 | /// the closure must have -- which is the only thing that types a closure |
| 363 | /// passed to a generic parameter. | 363 | /// passed to a generic parameter. |
| 364 | fn_blankets: HashMap<String, (Vec<String>, Vec<Nim>, Nim)>, | 364 | fn_blankets: HashMap<String, (Vec<String>, Vec<Nim>, Nim)>, |
| 365 | + /// Procs that cannot be translated, by Nim name, with the reason. | ||
| 366 | + /// | ||
| 367 | + /// Not an error on its own. A dependency graph carries far more code than | ||
| 368 | + /// any one program reaches -- 176,686 items behind libcosmic's 2,515 -- | ||
| 369 | + /// and refusing to translate the whole graph because one unreached | ||
| 370 | + /// function in it is untranslatable answers a question nobody asked. The | ||
| 371 | + /// error belongs at the call site, and that is where it is raised. | ||
| 372 | + untranslatable: HashMap<String, String>, | ||
| 365 | /// Bounds discovered while rewriting `impl Trait` parameters. `rewrite_apit` | 373 | /// Bounds discovered while rewriting `impl Trait` parameters. `rewrite_apit` |
| 366 | /// runs from `&self`, so they are parked here and folded into `bounds` by | 374 | /// runs from `&self`, so they are parked here and folded into `bounds` by |
| 367 | /// whoever called it. | 375 | /// whoever called it. |
| @@ -462,6 +470,7 @@ impl Lowerer { | |||
| 462 | opaque_why: None, | 470 | opaque_why: None, |
| 463 | trait_methods: HashMap::new(), | 471 | trait_methods: HashMap::new(), |
| 464 | fn_blankets: HashMap::new(), | 472 | fn_blankets: HashMap::new(), |
| 473 | + untranslatable: HashMap::new(), | ||
| 465 | pending_apit_bounds: std::cell::RefCell::new(Vec::new()), | 474 | pending_apit_bounds: std::cell::RefCell::new(Vec::new()), |
| 466 | bounds: HashMap::new(), | 475 | bounds: HashMap::new(), |
| 467 | const_names_at: std::cell::Cell::new(None), | 476 | const_names_at: std::cell::Cell::new(None), |
modified
src/main.rs +14 -0 | @@ -6,6 +6,7 @@ mod fmt; | ||
| 6 | 6 | mod macros; |
| 7 | 7 | mod mrules; |
| 8 | 8 | mod lower; |
| 9 | +mod prune; | |
| 9 | 10 | mod ty; |
| 10 | 11 | |
| 11 | 12 | use std::path::PathBuf; |
| @@ -69,6 +70,7 @@ fn run() -> Result<(), String> { | ||
| 69 | 70 | let mut features: Vec<String> = Vec::new(); |
| 70 | 71 | let mut cfgs: Vec<String> = Vec::new(); |
| 71 | 72 | let mut expand = false; |
| 73 | + let mut prune = false; | |
| 72 | 74 | |
| 73 | 75 | while let Some(a) = args.next() { |
| 74 | 76 | match a.to_string_lossy().as_ref() { |
| @@ -80,6 +82,7 @@ fn run() -> Result<(), String> { | ||
| 80 | 82 | ); |
| 81 | 83 | } |
| 82 | 84 | "--expand" => expand = true, |
| 85 | + "--prune" => prune = true, | |
| 83 | 86 | "--cfg" => { |
| 84 | 87 | let v = args.next().ok_or("`--cfg` needs an argument")?; |
| 85 | 88 | let v = v.to_string_lossy().into_owned(); |
| @@ -160,6 +163,17 @@ fn run() -> Result<(), String> { | ||
| 160 | 163 | files.push((module, parsed)); |
| 161 | 164 | } |
| 162 | 165 | |
| 166 | + // `--prune`: the first input is the program, the rest libraries, and a | |
| 167 | + // library item is kept only if something reachable names it. Off by | |
| 168 | + // default, because dropping code is not a thing to do silently. | |
| 169 | + if prune { | |
| 170 | + let (dropped, kept) = prune::prune(&mut files); | |
| 171 | + eprintln!( | |
| 172 | + "rustnim: pruned {dropped} unreached item(s) from {} librar(y|ies); {kept} kept", | |
| 173 | + files.len().saturating_sub(1) | |
| 174 | + ); | |
| 175 | + } | |
| 176 | + | |
| 163 | 177 | let mut lowerer = lower::Lowerer::new(); |
| 164 | 178 | lowerer.features = features; |
| 165 | 179 | lowerer.cfgs = cfgs; |
| @@ -6,6 +6,7 @@ mod fmt; | |||
| 6 | mod macros; | 6 | mod macros; |
| 7 | mod mrules; | 7 | mod mrules; |
| 8 | mod lower; | 8 | mod lower; |
| 9 | +mod prune; | ||
| 9 | mod ty; | 10 | mod ty; |
| 10 | 11 | ||
| 11 | use std::path::PathBuf; | 12 | use std::path::PathBuf; |
| @@ -69,6 +70,7 @@ fn run() -> Result<(), String> { | |||
| 69 | let mut features: Vec<String> = Vec::new(); | 70 | let mut features: Vec<String> = Vec::new(); |
| 70 | let mut cfgs: Vec<String> = Vec::new(); | 71 | let mut cfgs: Vec<String> = Vec::new(); |
| 71 | let mut expand = false; | 72 | let mut expand = false; |
| 73 | + let mut prune = false; | ||
| 72 | 74 | ||
| 73 | while let Some(a) = args.next() { | 75 | while let Some(a) = args.next() { |
| 74 | match a.to_string_lossy().as_ref() { | 76 | match a.to_string_lossy().as_ref() { |
| @@ -80,6 +82,7 @@ fn run() -> Result<(), String> { | |||
| 80 | ); | 82 | ); |
| 81 | } | 83 | } |
| 82 | "--expand" => expand = true, | 84 | "--expand" => expand = true, |
| 85 | + "--prune" => prune = true, | ||
| 83 | "--cfg" => { | 86 | "--cfg" => { |
| 84 | let v = args.next().ok_or("`--cfg` needs an argument")?; | 87 | let v = args.next().ok_or("`--cfg` needs an argument")?; |
| 85 | let v = v.to_string_lossy().into_owned(); | 88 | let v = v.to_string_lossy().into_owned(); |
| @@ -160,6 +163,17 @@ fn run() -> Result<(), String> { | |||
| 160 | files.push((module, parsed)); | 163 | files.push((module, parsed)); |
| 161 | } | 164 | } |
| 162 | 165 | ||
| 166 | + // `--prune`: the first input is the program, the rest libraries, and a | ||
| 167 | + // library item is kept only if something reachable names it. Off by | ||
| 168 | + // default, because dropping code is not a thing to do silently. | ||
| 169 | + if prune { | ||
| 170 | + let (dropped, kept) = prune::prune(&mut files); | ||
| 171 | + eprintln!( | ||
| 172 | + "rustnim: pruned {dropped} unreached item(s) from {} librar(y|ies); {kept} kept", | ||
| 173 | + files.len().saturating_sub(1) | ||
| 174 | + ); | ||
| 175 | + } | ||
| 176 | + | ||
| 163 | let mut lowerer = lower::Lowerer::new(); | 177 | let mut lowerer = lower::Lowerer::new(); |
| 164 | lowerer.features = features; | 178 | lowerer.features = features; |
| 165 | lowerer.cfgs = cfgs; | 179 | lowerer.cfgs = cfgs; |
added
src/prune.rs +166 -0 | new file mode 100644 | ||
| @@ -0,0 +1,166 @@ | ||
| 1 | +//! Keep only what the program reaches. | |
| 2 | +//! | |
| 3 | +//! A dependency graph carries far more code than any one program uses: | |
| 4 | +//! libcosmic's own crate declares 2,515 functions and the 356 crates behind it | |
| 5 | +//! declare 176,686 items. Translating all of them means answering questions | |
| 6 | +//! nobody asked -- and, because rustnim refuses rather than guesses, failing on | |
| 7 | +//! constructs in code that is never called. | |
| 8 | +//! | |
| 9 | +//! So the first input file is treated as the program and the rest as | |
| 10 | +//! libraries, and a library item is kept only if something reachable names it. | |
| 11 | +//! | |
| 12 | +//! Reachability is computed over *names*, not types. That is deliberately | |
| 13 | +//! coarse: it keeps more than is strictly needed, which is the safe direction. | |
| 14 | +//! Keeping too much costs translation effort; keeping too little would drop a | |
| 15 | +//! definition that something calls, and that surfaces as rustnim refusing an | |
| 16 | +//! unknown function -- loudly, at the call site, not as silently wrong output. | |
| 17 | + | |
| 18 | +use std::collections::HashSet; | |
| 19 | +use syn::Item; | |
| 20 | + | |
| 21 | +/// Every identifier mentioned anywhere in an item. | |
| 22 | +fn idents_of(item: &Item, out: &mut HashSet<String>) { | |
| 23 | + fn walk(ts: proc_macro2::TokenStream, out: &mut HashSet<String>) { | |
| 24 | + for t in ts { | |
| 25 | + match t { | |
| 26 | + proc_macro2::TokenTree::Ident(i) => { | |
| 27 | + out.insert(i.to_string()); | |
| 28 | + } | |
| 29 | + proc_macro2::TokenTree::Group(g) => walk(g.stream(), out), | |
| 30 | + _ => {} | |
| 31 | + } | |
| 32 | + } | |
| 33 | + } | |
| 34 | + walk(quote::quote!(#item), out); | |
| 35 | +} | |
| 36 | + | |
| 37 | +/// The names an item defines, by which something else can reach it. | |
| 38 | +fn defines(item: &Item) -> Vec<String> { | |
| 39 | + match item { | |
| 40 | + Item::Fn(f) => vec![f.sig.ident.to_string()], | |
| 41 | + Item::Struct(s) => vec![s.ident.to_string()], | |
| 42 | + Item::Union(u) => vec![u.ident.to_string()], | |
| 43 | + Item::Type(t) => vec![t.ident.to_string()], | |
| 44 | + Item::Const(c) => vec![c.ident.to_string()], | |
| 45 | + Item::Static(s) => vec![s.ident.to_string()], | |
| 46 | + Item::Trait(t) => { | |
| 47 | + // The trait, and every method on it: a call goes through the | |
| 48 | + // method name, which may be all the caller mentions. | |
| 49 | + let mut v = vec![t.ident.to_string()]; | |
| 50 | + for it in &t.items { | |
| 51 | + if let syn::TraitItem::Fn(f) = it { | |
| 52 | + v.push(f.sig.ident.to_string()); | |
| 53 | + } | |
| 54 | + } | |
| 55 | + v | |
| 56 | + } | |
| 57 | + Item::Enum(e) => { | |
| 58 | + // Variants too: `Shape::Circle` may be the only mention. | |
| 59 | + let mut v = vec![e.ident.to_string()]; | |
| 60 | + v.extend(e.variants.iter().map(|x| x.ident.to_string())); | |
| 61 | + v | |
| 62 | + } | |
| 63 | + Item::Impl(im) => { | |
| 64 | + // An impl is reachable through the type it is on, the trait it | |
| 65 | + // implements, or any method it declares. | |
| 66 | + let mut v = Vec::new(); | |
| 67 | + if let syn::Type::Path(p) = &*im.self_ty { | |
| 68 | + if let Some(seg) = p.path.segments.last() { | |
| 69 | + v.push(seg.ident.to_string()); | |
| 70 | + } | |
| 71 | + } | |
| 72 | + if let Some((path, _)) = &im.trait_ { | |
| 73 | + if let Some(seg) = path.segments.last() { | |
| 74 | + v.push(seg.ident.to_string()); | |
| 75 | + } | |
| 76 | + } | |
| 77 | + for it in &im.items { | |
| 78 | + match it { | |
| 79 | + syn::ImplItem::Fn(f) => v.push(f.sig.ident.to_string()), | |
| 80 | + syn::ImplItem::Const(c) => v.push(c.ident.to_string()), | |
| 81 | + _ => {} | |
| 82 | + } | |
| 83 | + } | |
| 84 | + v | |
| 85 | + } | |
| 86 | + Item::Macro(m) => m.ident.iter().map(|i| i.to_string()).collect(), | |
| 87 | + _ => Vec::new(), | |
| 88 | + } | |
| 89 | +} | |
| 90 | + | |
| 91 | +/// Flatten inline modules so an item inside one is prunable on its own. | |
| 92 | +/// `mod` is a namespace, and the lowering already flattens it. | |
| 93 | +fn flatten(items: Vec<Item>) -> Vec<Item> { | |
| 94 | + let mut out = Vec::new(); | |
| 95 | + for item in items { | |
| 96 | + match item { | |
| 97 | + Item::Mod(m) if m.content.is_some() => { | |
| 98 | + let inner = m.content.unwrap().1; | |
| 99 | + out.extend(flatten(inner)); | |
| 100 | + } | |
| 101 | + other => out.push(other), | |
| 102 | + } | |
| 103 | + } | |
| 104 | + out | |
| 105 | +} | |
| 106 | + | |
| 107 | +/// Drop library items nothing in the program reaches. | |
| 108 | +/// | |
| 109 | +/// Returns how many items were dropped and how many kept, so the caller can | |
| 110 | +/// say -- a pruning pass that quietly removed the wrong thing should not be | |
| 111 | +/// invisible. | |
| 112 | +pub fn prune(files: &mut [(String, syn::File)]) -> (usize, usize) { | |
| 113 | + if files.len() < 2 { | |
| 114 | + return (0, 0); | |
| 115 | + } | |
| 116 | + // The program is the first input; everything else is a library. | |
| 117 | + let mut wanted: HashSet<String> = HashSet::new(); | |
| 118 | + for item in &files[0].1.items { | |
| 119 | + idents_of(item, &mut wanted); | |
| 120 | + } | |
| 121 | + | |
| 122 | + let lib: Vec<Vec<Item>> = files[1..] | |
| 123 | + .iter() | |
| 124 | + .map(|(_, f)| flatten(f.items.clone())) | |
| 125 | + .collect(); | |
| 126 | + let names: Vec<Vec<Vec<String>>> = lib | |
| 127 | + .iter() | |
| 128 | + .map(|items| items.iter().map(defines).collect()) | |
| 129 | + .collect(); | |
| 130 | + | |
| 131 | + let mut keep: Vec<Vec<bool>> = lib.iter().map(|i| vec![false; i.len()]).collect(); | |
| 132 | + loop { | |
| 133 | + let mut grew = false; | |
| 134 | + for (fi, items) in lib.iter().enumerate() { | |
| 135 | + for (ii, item) in items.iter().enumerate() { | |
| 136 | + if keep[fi][ii] { | |
| 137 | + continue; | |
| 138 | + } | |
| 139 | + if names[fi][ii].iter().any(|n| wanted.contains(n)) { | |
| 140 | + keep[fi][ii] = true; | |
| 141 | + idents_of(item, &mut wanted); | |
| 142 | + grew = true; | |
| 143 | + } | |
| 144 | + } | |
| 145 | + } | |
| 146 | + if !grew { | |
| 147 | + break; | |
| 148 | + } | |
| 149 | + } | |
| 150 | + | |
| 151 | + let mut dropped = 0; | |
| 152 | + let mut kept = 0; | |
| 153 | + for (fi, items) in lib.into_iter().enumerate() { | |
| 154 | + let mut out = Vec::new(); | |
| 155 | + for (ii, item) in items.into_iter().enumerate() { | |
| 156 | + if keep[fi][ii] { | |
| 157 | + kept += 1; | |
| 158 | + out.push(item); | |
| 159 | + } else { | |
| 160 | + dropped += 1; | |
| 161 | + } | |
| 162 | + } | |
| 163 | + files[fi + 1].1.items = out; | |
| 164 | + } | |
| 165 | + (dropped, kept) | |
| 166 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,166 @@ | |||
| 1 | +//! Keep only what the program reaches. | ||
| 2 | +//! | ||
| 3 | +//! A dependency graph carries far more code than any one program uses: | ||
| 4 | +//! libcosmic's own crate declares 2,515 functions and the 356 crates behind it | ||
| 5 | +//! declare 176,686 items. Translating all of them means answering questions | ||
| 6 | +//! nobody asked -- and, because rustnim refuses rather than guesses, failing on | ||
| 7 | +//! constructs in code that is never called. | ||
| 8 | +//! | ||
| 9 | +//! So the first input file is treated as the program and the rest as | ||
| 10 | +//! libraries, and a library item is kept only if something reachable names it. | ||
| 11 | +//! | ||
| 12 | +//! Reachability is computed over *names*, not types. That is deliberately | ||
| 13 | +//! coarse: it keeps more than is strictly needed, which is the safe direction. | ||
| 14 | +//! Keeping too much costs translation effort; keeping too little would drop a | ||
| 15 | +//! definition that something calls, and that surfaces as rustnim refusing an | ||
| 16 | +//! unknown function -- loudly, at the call site, not as silently wrong output. | ||
| 17 | + | ||
| 18 | +use std::collections::HashSet; | ||
| 19 | +use syn::Item; | ||
| 20 | + | ||
| 21 | +/// Every identifier mentioned anywhere in an item. | ||
| 22 | +fn idents_of(item: &Item, out: &mut HashSet<String>) { | ||
| 23 | + fn walk(ts: proc_macro2::TokenStream, out: &mut HashSet<String>) { | ||
| 24 | + for t in ts { | ||
| 25 | + match t { | ||
| 26 | + proc_macro2::TokenTree::Ident(i) => { | ||
| 27 | + out.insert(i.to_string()); | ||
| 28 | + } | ||
| 29 | + proc_macro2::TokenTree::Group(g) => walk(g.stream(), out), | ||
| 30 | + _ => {} | ||
| 31 | + } | ||
| 32 | + } | ||
| 33 | + } | ||
| 34 | + walk(quote::quote!(#item), out); | ||
| 35 | +} | ||
| 36 | + | ||
| 37 | +/// The names an item defines, by which something else can reach it. | ||
| 38 | +fn defines(item: &Item) -> Vec<String> { | ||
| 39 | + match item { | ||
| 40 | + Item::Fn(f) => vec![f.sig.ident.to_string()], | ||
| 41 | + Item::Struct(s) => vec![s.ident.to_string()], | ||
| 42 | + Item::Union(u) => vec![u.ident.to_string()], | ||
| 43 | + Item::Type(t) => vec![t.ident.to_string()], | ||
| 44 | + Item::Const(c) => vec![c.ident.to_string()], | ||
| 45 | + Item::Static(s) => vec![s.ident.to_string()], | ||
| 46 | + Item::Trait(t) => { | ||
| 47 | + // The trait, and every method on it: a call goes through the | ||
| 48 | + // method name, which may be all the caller mentions. | ||
| 49 | + let mut v = vec![t.ident.to_string()]; | ||
| 50 | + for it in &t.items { | ||
| 51 | + if let syn::TraitItem::Fn(f) = it { | ||
| 52 | + v.push(f.sig.ident.to_string()); | ||
| 53 | + } | ||
| 54 | + } | ||
| 55 | + v | ||
| 56 | + } | ||
| 57 | + Item::Enum(e) => { | ||
| 58 | + // Variants too: `Shape::Circle` may be the only mention. | ||
| 59 | + let mut v = vec![e.ident.to_string()]; | ||
| 60 | + v.extend(e.variants.iter().map(|x| x.ident.to_string())); | ||
| 61 | + v | ||
| 62 | + } | ||
| 63 | + Item::Impl(im) => { | ||
| 64 | + // An impl is reachable through the type it is on, the trait it | ||
| 65 | + // implements, or any method it declares. | ||
| 66 | + let mut v = Vec::new(); | ||
| 67 | + if let syn::Type::Path(p) = &*im.self_ty { | ||
| 68 | + if let Some(seg) = p.path.segments.last() { | ||
| 69 | + v.push(seg.ident.to_string()); | ||
| 70 | + } | ||
| 71 | + } | ||
| 72 | + if let Some((path, _)) = &im.trait_ { | ||
| 73 | + if let Some(seg) = path.segments.last() { | ||
| 74 | + v.push(seg.ident.to_string()); | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + for it in &im.items { | ||
| 78 | + match it { | ||
| 79 | + syn::ImplItem::Fn(f) => v.push(f.sig.ident.to_string()), | ||
| 80 | + syn::ImplItem::Const(c) => v.push(c.ident.to_string()), | ||
| 81 | + _ => {} | ||
| 82 | + } | ||
| 83 | + } | ||
| 84 | + v | ||
| 85 | + } | ||
| 86 | + Item::Macro(m) => m.ident.iter().map(|i| i.to_string()).collect(), | ||
| 87 | + _ => Vec::new(), | ||
| 88 | + } | ||
| 89 | +} | ||
| 90 | + | ||
| 91 | +/// Flatten inline modules so an item inside one is prunable on its own. | ||
| 92 | +/// `mod` is a namespace, and the lowering already flattens it. | ||
| 93 | +fn flatten(items: Vec<Item>) -> Vec<Item> { | ||
| 94 | + let mut out = Vec::new(); | ||
| 95 | + for item in items { | ||
| 96 | + match item { | ||
| 97 | + Item::Mod(m) if m.content.is_some() => { | ||
| 98 | + let inner = m.content.unwrap().1; | ||
| 99 | + out.extend(flatten(inner)); | ||
| 100 | + } | ||
| 101 | + other => out.push(other), | ||
| 102 | + } | ||
| 103 | + } | ||
| 104 | + out | ||
| 105 | +} | ||
| 106 | + | ||
| 107 | +/// Drop library items nothing in the program reaches. | ||
| 108 | +/// | ||
| 109 | +/// Returns how many items were dropped and how many kept, so the caller can | ||
| 110 | +/// say -- a pruning pass that quietly removed the wrong thing should not be | ||
| 111 | +/// invisible. | ||
| 112 | +pub fn prune(files: &mut [(String, syn::File)]) -> (usize, usize) { | ||
| 113 | + if files.len() < 2 { | ||
| 114 | + return (0, 0); | ||
| 115 | + } | ||
| 116 | + // The program is the first input; everything else is a library. | ||
| 117 | + let mut wanted: HashSet<String> = HashSet::new(); | ||
| 118 | + for item in &files[0].1.items { | ||
| 119 | + idents_of(item, &mut wanted); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + let lib: Vec<Vec<Item>> = files[1..] | ||
| 123 | + .iter() | ||
| 124 | + .map(|(_, f)| flatten(f.items.clone())) | ||
| 125 | + .collect(); | ||
| 126 | + let names: Vec<Vec<Vec<String>>> = lib | ||
| 127 | + .iter() | ||
| 128 | + .map(|items| items.iter().map(defines).collect()) | ||
| 129 | + .collect(); | ||
| 130 | + | ||
| 131 | + let mut keep: Vec<Vec<bool>> = lib.iter().map(|i| vec![false; i.len()]).collect(); | ||
| 132 | + loop { | ||
| 133 | + let mut grew = false; | ||
| 134 | + for (fi, items) in lib.iter().enumerate() { | ||
| 135 | + for (ii, item) in items.iter().enumerate() { | ||
| 136 | + if keep[fi][ii] { | ||
| 137 | + continue; | ||
| 138 | + } | ||
| 139 | + if names[fi][ii].iter().any(|n| wanted.contains(n)) { | ||
| 140 | + keep[fi][ii] = true; | ||
| 141 | + idents_of(item, &mut wanted); | ||
| 142 | + grew = true; | ||
| 143 | + } | ||
| 144 | + } | ||
| 145 | + } | ||
| 146 | + if !grew { | ||
| 147 | + break; | ||
| 148 | + } | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + let mut dropped = 0; | ||
| 152 | + let mut kept = 0; | ||
| 153 | + for (fi, items) in lib.into_iter().enumerate() { | ||
| 154 | + let mut out = Vec::new(); | ||
| 155 | + for (ii, item) in items.into_iter().enumerate() { | ||
| 156 | + if keep[fi][ii] { | ||
| 157 | + kept += 1; | ||
| 158 | + out.push(item); | ||
| 159 | + } else { | ||
| 160 | + dropped += 1; | ||
| 161 | + } | ||
| 162 | + } | ||
| 163 | + files[fi + 1].1.items = out; | ||
| 164 | + } | ||
| 165 | + (dropped, kept) | ||
| 166 | +} | ||
added
tests/cases/065-prune/lib.rs +28 -0 | new file mode 100644 | ||
| @@ -0,0 +1,28 @@ | ||
| 1 | +// A library with far more in it than the program uses — including a function | |
| 2 | +// rustnim cannot translate at all. Nothing reaches it, so nothing should | |
| 3 | +// notice. | |
| 4 | +pub fn used(x: u32) -> u32 { | |
| 5 | + helper(x) + 1 | |
| 6 | +} | |
| 7 | + | |
| 8 | +pub fn helper(x: u32) -> u32 { | |
| 9 | + x * 2 | |
| 10 | +} | |
| 11 | + | |
| 12 | +pub struct Used { | |
| 13 | + pub v: u32, | |
| 14 | +} | |
| 15 | + | |
| 16 | +pub struct Unused { | |
| 17 | + pub v: u128, | |
| 18 | +} | |
| 19 | + | |
| 20 | +pub fn never_called() -> u32 { | |
| 21 | + // `std::process::id` is not in the supported subset; translating this | |
| 22 | + // would fail. It is unreachable, so it is not translated. | |
| 23 | + std::process::id() | |
| 24 | +} | |
| 25 | + | |
| 26 | +pub fn also_unused(u: &Unused) -> u128 { | |
| 27 | + u.v | |
| 28 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | +// A library with far more in it than the program uses — including a function | ||
| 2 | +// rustnim cannot translate at all. Nothing reaches it, so nothing should | ||
| 3 | +// notice. | ||
| 4 | +pub fn used(x: u32) -> u32 { | ||
| 5 | + helper(x) + 1 | ||
| 6 | +} | ||
| 7 | + | ||
| 8 | +pub fn helper(x: u32) -> u32 { | ||
| 9 | + x * 2 | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +pub struct Used { | ||
| 13 | + pub v: u32, | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +pub struct Unused { | ||
| 17 | + pub v: u128, | ||
| 18 | +} | ||
| 19 | + | ||
| 20 | +pub fn never_called() -> u32 { | ||
| 21 | + // `std::process::id` is not in the supported subset; translating this | ||
| 22 | + // would fail. It is unreachable, so it is not translated. | ||
| 23 | + std::process::id() | ||
| 24 | +} | ||
| 25 | + | ||
| 26 | +pub fn also_unused(u: &Unused) -> u128 { | ||
| 27 | + u.v | ||
| 28 | +} | ||
added
tests/cases/065-prune/main.rs +10 -0 | new file mode 100644 | ||
| @@ -0,0 +1,10 @@ | ||
| 1 | +//@ prune | |
| 2 | +// Only `used`, `helper` and `Used` are reachable from here. | |
| 3 | +mod lib; | |
| 4 | + | |
| 5 | +use lib::Used; | |
| 6 | + | |
| 7 | +fn main() { | |
| 8 | + let u = Used { v: 20 }; | |
| 9 | + println!("{}", lib::used(u.v)); | |
| 10 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | +//@ prune | ||
| 2 | +// Only `used`, `helper` and `Used` are reachable from here. | ||
| 3 | +mod lib; | ||
| 4 | + | ||
| 5 | +use lib::Used; | ||
| 6 | + | ||
| 7 | +fn main() { | ||
| 8 | + let u = Used { v: 20 }; | ||
| 9 | + println!("{}", lib::used(u.v)); | ||
| 10 | +} | ||
modified
tests/differential.rs +8 -0 | @@ -26,6 +26,7 @@ | ||
| 26 | 26 | //! //@ skip: <reason> Not run; reported as skipped. |
| 27 | 27 | //! //@ args: <argv> Passed to both binaries. |
| 28 | 28 | //! //@ stdin: <line> Fed to both binaries on stdin. |
| 29 | +//! //@ prune rustnim drops library items nothing reaches. | |
| 29 | 30 | |
| 30 | 31 | use std::collections::BTreeMap; |
| 31 | 32 | use std::fmt::Write as _; |
| @@ -137,6 +138,9 @@ struct Directives { | ||
| 137 | 138 | /// Transpile the *expanded* source: rustnim runs rustc's macro expansion |
| 138 | 139 | /// first, so what is lowered is what the derives generated. |
| 139 | 140 | expand: bool, |
| 141 | + /// Pass `--prune` to rustnim: the first input is the program and the rest | |
| 142 | + /// libraries, and unreached library items are dropped. | |
| 143 | + prune: bool, | |
| 140 | 144 | /// Crates the *oracle* must link. rustnim does not use them — the point |
| 141 | 145 | /// of such a case is that rustnim reproduces the crate's behaviour |
| 142 | 146 | /// without it. |
| @@ -167,6 +171,7 @@ fn directives(src: &str) -> Directives { | ||
| 167 | 171 | "cfg" => d.cfg.push(val), |
| 168 | 172 | "extern" => d.externs.push(val), |
| 169 | 173 | "expand" => d.expand = true, |
| 174 | + "prune" => d.prune = true, | |
| 170 | 175 | "stdin" => d.stdin = Some(format!("{val}\n")), |
| 171 | 176 | _ => {} |
| 172 | 177 | } |
| @@ -289,6 +294,9 @@ fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome { | ||
| 289 | 294 | ); |
| 290 | 295 | let nim_src = dir.join(format!("{mod_name}.nim")); |
| 291 | 296 | let mut cmd = Command::new(RUSTNIM); |
| 297 | + if d.prune { | |
| 298 | + cmd.arg("--prune"); | |
| 299 | + } | |
| 292 | 300 | if d.expand { |
| 293 | 301 | cmd.arg("--expand"); |
| 294 | 302 | } |
| @@ -26,6 +26,7 @@ | |||
| 26 | //! //@ skip: <reason> Not run; reported as skipped. | 26 | //! //@ skip: <reason> Not run; reported as skipped. |
| 27 | //! //@ args: <argv> Passed to both binaries. | 27 | //! //@ args: <argv> Passed to both binaries. |
| 28 | //! //@ stdin: <line> Fed to both binaries on stdin. | 28 | //! //@ stdin: <line> Fed to both binaries on stdin. |
| 29 | +//! //@ prune rustnim drops library items nothing reaches. | ||
| 29 | 30 | ||
| 30 | use std::collections::BTreeMap; | 31 | use std::collections::BTreeMap; |
| 31 | use std::fmt::Write as _; | 32 | use std::fmt::Write as _; |
| @@ -137,6 +138,9 @@ struct Directives { | |||
| 137 | /// Transpile the *expanded* source: rustnim runs rustc's macro expansion | 138 | /// Transpile the *expanded* source: rustnim runs rustc's macro expansion |
| 138 | /// first, so what is lowered is what the derives generated. | 139 | /// first, so what is lowered is what the derives generated. |
| 139 | expand: bool, | 140 | expand: bool, |
| 141 | + /// Pass `--prune` to rustnim: the first input is the program and the rest | ||
| 142 | + /// libraries, and unreached library items are dropped. | ||
| 143 | + prune: bool, | ||
| 140 | /// Crates the *oracle* must link. rustnim does not use them — the point | 144 | /// Crates the *oracle* must link. rustnim does not use them — the point |
| 141 | /// of such a case is that rustnim reproduces the crate's behaviour | 145 | /// of such a case is that rustnim reproduces the crate's behaviour |
| 142 | /// without it. | 146 | /// without it. |
| @@ -167,6 +171,7 @@ fn directives(src: &str) -> Directives { | |||
| 167 | "cfg" => d.cfg.push(val), | 171 | "cfg" => d.cfg.push(val), |
| 168 | "extern" => d.externs.push(val), | 172 | "extern" => d.externs.push(val), |
| 169 | "expand" => d.expand = true, | 173 | "expand" => d.expand = true, |
| 174 | + "prune" => d.prune = true, | ||
| 170 | "stdin" => d.stdin = Some(format!("{val}\n")), | 175 | "stdin" => d.stdin = Some(format!("{val}\n")), |
| 171 | _ => {} | 176 | _ => {} |
| 172 | } | 177 | } |
| @@ -289,6 +294,9 @@ fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome { | |||
| 289 | ); | 294 | ); |
| 290 | let nim_src = dir.join(format!("{mod_name}.nim")); | 295 | let nim_src = dir.join(format!("{mod_name}.nim")); |
| 291 | let mut cmd = Command::new(RUSTNIM); | 296 | let mut cmd = Command::new(RUSTNIM); |
| 297 | + if d.prune { | ||
| 298 | + cmd.arg("--prune"); | ||
| 299 | + } | ||
| 292 | if d.expand { | 300 | if d.expand { |
| 293 | cmd.arg("--expand"); | 301 | cmd.arg("--expand"); |
| 294 | } | 302 | } |