nandi/rustnimpublic Fork
⑂ fefccb9
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/rustnim.git
git clone ssh://git@rickub.com/nandi/rustnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

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>
nandithebull committed 2026-09-19T20:58:15-07:00 Browse files
fefccb9 parent: 2cccbf7
modified src/lower.rs +9 -0
@@ -362,6 +362,14 @@ pub struct Lowerer {
362362 /// the closure must have -- which is the only thing that types a closure
363363 /// passed to a generic parameter.
364364 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>,
365373 /// Bounds discovered while rewriting `impl Trait` parameters. `rewrite_apit`
366374 /// runs from `&self`, so they are parked here and folded into `bounds` by
367375 /// whoever called it.
@@ -462,6 +470,7 @@ impl Lowerer {
462470 opaque_why: None,
463471 trait_methods: HashMap::new(),
464472 fn_blankets: HashMap::new(),
473+ untranslatable: HashMap::new(),
465474 pending_apit_bounds: std::cell::RefCell::new(Vec::new()),
466475 bounds: HashMap::new(),
467476 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 closure362 /// 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` by374 /// 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;
66 mod macros;
77 mod mrules;
88 mod lower;
9+mod prune;
910 mod ty;
1011
1112 use std::path::PathBuf;
@@ -69,6 +70,7 @@ fn run() -> Result<(), String> {
6970 let mut features: Vec<String> = Vec::new();
7071 let mut cfgs: Vec<String> = Vec::new();
7172 let mut expand = false;
73+ let mut prune = false;
7274
7375 while let Some(a) = args.next() {
7476 match a.to_string_lossy().as_ref() {
@@ -80,6 +82,7 @@ fn run() -> Result<(), String> {
8082 );
8183 }
8284 "--expand" => expand = true,
85+ "--prune" => prune = true,
8386 "--cfg" => {
8487 let v = args.next().ok_or("`--cfg` needs an argument")?;
8588 let v = v.to_string_lossy().into_owned();
@@ -160,6 +163,17 @@ fn run() -> Result<(), String> {
160163 files.push((module, parsed));
161164 }
162165
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+
163177 let mut lowerer = lower::Lowerer::new();
164178 lowerer.features = features;
165179 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 @@
2626 //! //@ skip: <reason> Not run; reported as skipped.
2727 //! //@ args: <argv> Passed to both binaries.
2828 //! //@ stdin: <line> Fed to both binaries on stdin.
29+//! //@ prune rustnim drops library items nothing reaches.
2930
3031 use std::collections::BTreeMap;
3132 use std::fmt::Write as _;
@@ -137,6 +138,9 @@ struct Directives {
137138 /// Transpile the *expanded* source: rustnim runs rustc's macro expansion
138139 /// first, so what is lowered is what the derives generated.
139140 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,
140144 /// Crates the *oracle* must link. rustnim does not use them — the point
141145 /// of such a case is that rustnim reproduces the crate's behaviour
142146 /// without it.
@@ -167,6 +171,7 @@ fn directives(src: &str) -> Directives {
167171 "cfg" => d.cfg.push(val),
168172 "extern" => d.externs.push(val),
169173 "expand" => d.expand = true,
174+ "prune" => d.prune = true,
170175 "stdin" => d.stdin = Some(format!("{val}\n")),
171176 _ => {}
172177 }
@@ -289,6 +294,9 @@ fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {
289294 );
290295 let nim_src = dir.join(format!("{mod_name}.nim"));
291296 let mut cmd = Command::new(RUSTNIM);
297+ if d.prune {
298+ cmd.arg("--prune");
299+ }
292300 if d.expand {
293301 cmd.arg("--expand");
294302 }
@@ -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 expansion138 /// 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 point144 /// Crates the *oracle* must link. rustnim does not use them — the point
141 /// of such a case is that rustnim reproduces the crate's behaviour145 /// 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 }