nandi/rustnimpublic Fork 0
0777223
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.

Fail when nothing was translated, not just when the file is empty

Running rustnim over libcosmic turned up three files that "succeeded": exit
0, a 6,984-byte output file, and zero translated items. Every item in them
was gated behind a feature that is off, so they were correctly dropped -- and
then the prelude alone was written out as if it were a translation.

That is the failure in findings/ wearing a disguise. The existing guard
checks the output file is non-empty, which the prelude satisfies on its own.
So the count is now of items actually emitted, and zero is an error that
names how many were dropped by #[cfg] and how to enable them.

tests/cases/905 pins it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-18T21:15:33-07:00 Browse files
0777223 parent: 5fdd5be
modified src/lower.rs +27 -0
@@ -237,6 +237,10 @@ pub struct Lowerer {
237237 /// Module names supplied as separate input files. A `mod x;` naming one
238238 /// of these is satisfied by that file having been passed in.
239239 pub modules: Vec<String>,
240+ /// How many items were actually translated. If this is zero the input
241+ /// produced nothing but the prelude, and reporting success for that is
242+ /// the precise failure this project exists to avoid -- see `findings/`.
243+ emitted: usize,
240244 /// Cargo features that are on, as `--cfg feature=<name>`. `#[cfg]` is
241245 /// evaluated against these exactly as rustc would, so an item that is
242246 /// dropped here is genuinely not part of the program being compiled.
@@ -278,6 +282,7 @@ impl Lowerer {
278282 forwards: Vec::new(),
279283 aliases: HashMap::new(),
280284 modules: Vec::new(),
285+ emitted: 0,
281286 features: Vec::new(),
282287 dropped_by_cfg: 0,
283288 ret: None,
@@ -385,6 +390,25 @@ impl Lowerer {
385390 }
386391 }
387392
393+ // An input that translates to nothing is a failure, however plausible
394+ // the output file looks. The prelude alone is not a translation.
395+ if self.emitted == 0 {
396+ return Err(format!(
397+ "nothing was translated: the input has no items this lowering \
398+ emits{}. Writing a file containing only the prelude would \
399+ report success for work that was not done",
400+ if self.dropped_by_cfg > 0 {
401+ format!(
402+ " ({} item(s) were dropped by `#[cfg]`; enable them with \
403+ `--cfg feature=<name>`)",
404+ self.dropped_by_cfg
405+ )
406+ } else {
407+ String::new()
408+ }
409+ ));
410+ }
411+
388412 if self.fns.contains_key(&(String::new(), "main".to_string())) {
389413 self.blank();
390414 self.line("when isMainModule:");
@@ -934,6 +958,9 @@ impl Lowerer {
934958 }
935959
936960 fn item_inner(&mut self, item: &Item) -> Result<(), String> {
961+ if !matches!(item, Item::Use(_) | Item::ExternCrate(_) | Item::Mod(_) | Item::Type(_)) {
962+ self.emitted += 1;
963+ }
937964 match item {
938965 Item::Fn(f) => {
939966 let nim = self.fn_name(&self.cur_mod, &f.sig.ident.to_string());
@@ -237,6 +237,10 @@ pub struct Lowerer {
237 /// Module names supplied as separate input files. A `mod x;` naming one237 /// Module names supplied as separate input files. A `mod x;` naming one
238 /// of these is satisfied by that file having been passed in.238 /// of these is satisfied by that file having been passed in.
239 pub modules: Vec<String>,239 pub modules: Vec<String>,
240+ /// How many items were actually translated. If this is zero the input
241+ /// produced nothing but the prelude, and reporting success for that is
242+ /// the precise failure this project exists to avoid -- see `findings/`.
243+ emitted: usize,
240 /// Cargo features that are on, as `--cfg feature=<name>`. `#[cfg]` is244 /// Cargo features that are on, as `--cfg feature=<name>`. `#[cfg]` is
241 /// evaluated against these exactly as rustc would, so an item that is245 /// evaluated against these exactly as rustc would, so an item that is
242 /// dropped here is genuinely not part of the program being compiled.246 /// dropped here is genuinely not part of the program being compiled.
@@ -278,6 +282,7 @@ impl Lowerer {
278 forwards: Vec::new(),282 forwards: Vec::new(),
279 aliases: HashMap::new(),283 aliases: HashMap::new(),
280 modules: Vec::new(),284 modules: Vec::new(),
285+ emitted: 0,
281 features: Vec::new(),286 features: Vec::new(),
282 dropped_by_cfg: 0,287 dropped_by_cfg: 0,
283 ret: None,288 ret: None,
@@ -385,6 +390,25 @@ impl Lowerer {
385 }390 }
386 }391 }
387 392
393+ // An input that translates to nothing is a failure, however plausible
394+ // the output file looks. The prelude alone is not a translation.
395+ if self.emitted == 0 {
396+ return Err(format!(
397+ "nothing was translated: the input has no items this lowering \
398+ emits{}. Writing a file containing only the prelude would \
399+ report success for work that was not done",
400+ if self.dropped_by_cfg > 0 {
401+ format!(
402+ " ({} item(s) were dropped by `#[cfg]`; enable them with \
403+ `--cfg feature=<name>`)",
404+ self.dropped_by_cfg
405+ )
406+ } else {
407+ String::new()
408+ }
409+ ));
410+ }
411+
388 if self.fns.contains_key(&(String::new(), "main".to_string())) {412 if self.fns.contains_key(&(String::new(), "main".to_string())) {
389 self.blank();413 self.blank();
390 self.line("when isMainModule:");414 self.line("when isMainModule:");
@@ -934,6 +958,9 @@ impl Lowerer {
934 }958 }
935 959
936 fn item_inner(&mut self, item: &Item) -> Result<(), String> {960 fn item_inner(&mut self, item: &Item) -> Result<(), String> {
961+ if !matches!(item, Item::Use(_) | Item::ExternCrate(_) | Item::Mod(_) | Item::Type(_)) {
962+ self.emitted += 1;
963+ }
937 match item {964 match item {
938 Item::Fn(f) => {965 Item::Fn(f) => {
939 let nim = self.fn_name(&self.cur_mod, &f.sig.ident.to_string());966 let nim = self.fn_name(&self.cur_mod, &f.sig.ident.to_string());
added tests/cases/905-reject-nothing-translated.rs +8 -0
new file mode 100644
@@ -0,0 +1,8 @@
1+//@ reject: nothing was translated
2+// Exiting 0 with a file containing only the prelude would report success for
3+// work that was not done. That is the failure in `findings/`, and it is not
4+// caught by checking the output file is non-empty.
5+#[cfg(feature = "something")]
6+pub fn only_under_a_feature() -> i32 {
7+ 1
8+}
new file mode 100644
@@ -0,0 +1,8 @@
1+//@ reject: nothing was translated
2+// Exiting 0 with a file containing only the prelude would report success for
3+// work that was not done. That is the failure in `findings/`, and it is not
4+// caught by checking the output file is non-empty.
5+#[cfg(feature = "something")]
6+pub fn only_under_a_feature() -> i32 {
7+ 1
8+}