Skip to content

Commit 2e6a903

Browse files
randomPoisonthedataking
authored andcommitted
transpile: handle module name collisions
1 parent e1c2970 commit 2e6a903

8 files changed

Lines changed: 243 additions & 13 deletions

File tree

c2rust-transpile/src/build_files/mod.rs

Lines changed: 119 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use std::collections::BTreeMap;
1+
use std::collections::{BTreeMap, BTreeSet};
22
use std::fs::{self, File};
33
use std::io::Write;
4+
use std::ops::Bound::{Excluded, Unbounded};
45
use std::path::{Path, PathBuf};
56
use std::str::FromStr;
67

@@ -107,7 +108,7 @@ pub fn emit_build_files<'lcmd>(
107108
})
108109
}
109110

110-
#[derive(Serialize)]
111+
#[derive(Clone, Debug, Serialize)]
111112
struct Module {
112113
path: Option<String>,
113114
name: String,
@@ -116,19 +117,23 @@ struct Module {
116117
}
117118

118119
#[derive(Debug, Default)]
119-
struct ModuleTree(BTreeMap<String, ModuleTree>);
120+
struct ModuleTree {
121+
children: BTreeMap<String, ModuleTree>,
122+
path_modules: Vec<Module>,
123+
}
120124

121125
impl ModuleTree {
122126
/// Convert the tree representation into a linear vector
123127
/// and push it into `res`
124128
fn linearize(&self, res: &mut Vec<Module>) {
125-
for (name, child) in self.0.iter() {
129+
res.extend(self.path_modules.iter().cloned());
130+
for (name, child) in self.children.iter() {
126131
child.linearize_internal(name, res);
127132
}
128133
}
129134

130135
fn linearize_internal(&self, name: &str, res: &mut Vec<Module>) {
131-
if self.0.is_empty() {
136+
if self.path_modules.is_empty() && self.children.is_empty() {
132137
res.push(Module {
133138
name: name.to_string(),
134139
path: None,
@@ -151,6 +156,55 @@ impl ModuleTree {
151156
});
152157
}
153158
}
159+
160+
fn insert_nested(&mut self, path: Vec<String>) {
161+
let mut cur = self;
162+
for name in path {
163+
cur = cur.children.entry(name).or_default();
164+
}
165+
}
166+
167+
fn insert_path_module(
168+
&mut self,
169+
parent_path: &[String],
170+
file_name: String,
171+
module_name: String,
172+
) {
173+
let mut cur = self;
174+
for name in parent_path {
175+
cur = cur.children.entry(name.clone()).or_default();
176+
}
177+
let name = cur.unique_child_name(&module_name);
178+
cur.path_modules.push(Module {
179+
path: Some(file_name),
180+
name,
181+
open: false,
182+
close: false,
183+
});
184+
}
185+
186+
fn unique_child_name(&self, module_name: &str) -> String {
187+
if !self.child_name_exists(module_name) {
188+
return module_name.to_owned();
189+
}
190+
191+
for i in 1.. {
192+
let candidate = format!("{module_name}_{i}");
193+
if !self.child_name_exists(&candidate) {
194+
return candidate;
195+
}
196+
}
197+
198+
unreachable!("We tried all the numbers and couldn't find one that didn't collide")
199+
}
200+
201+
fn child_name_exists(&self, module_name: &str) -> bool {
202+
self.children.contains_key(module_name)
203+
|| self
204+
.path_modules
205+
.iter()
206+
.any(|module| module.name == module_name)
207+
}
154208
}
155209

156210
#[derive(Debug, PartialEq, Eq)]
@@ -182,17 +236,51 @@ fn convert_module_list(
182236
});
183237

184238
let mut res = vec![];
185-
let mut module_tree = ModuleTree(BTreeMap::new());
239+
let mut module_tree = ModuleTree::default();
240+
// A C file can have the same stem as a sibling directory, such as
241+
// `hash.c` and `hash/sha1.c`. The current nested module emitter cannot
242+
// represent both at `mod hash`, so emit the file module with an explicit
243+
// path and keep the directory in the nested tree.
244+
let internal_modules = modules
245+
.iter()
246+
.filter_map(|m| {
247+
if tcfg.is_binary(m) {
248+
return None;
249+
}
250+
let relpath = m.strip_prefix(build_dir).ok()?;
251+
let path = module_path(relpath);
252+
Some((m, path))
253+
})
254+
.collect::<Vec<_>>();
255+
// A module collides when another module's path strictly extends it, i.e. a
256+
// file whose stem matches a sibling directory (`hash.c` next to `hash/`).
257+
// Every strict extension of a path sorts immediately after it, so checking
258+
// each path's immediate successor in the sorted set settles it without
259+
// comparing every pair.
260+
let module_paths = internal_modules
261+
.iter()
262+
.map(|(_, path)| path.clone())
263+
.collect::<BTreeSet<_>>();
264+
let collision_modules = internal_modules
265+
.iter()
266+
.filter(|(_, path)| {
267+
module_paths
268+
.range::<Vec<String>, _>((Excluded(path), Unbounded))
269+
.next()
270+
.is_some_and(|next| next.len() > path.len() && next.starts_with(path))
271+
})
272+
.map(|(module, _)| *module)
273+
.collect::<BTreeSet<_>>();
274+
275+
let mut collision_relpaths = vec![];
186276
for m in &modules {
187277
match m.strip_prefix(build_dir) {
188-
Ok(relpath) if !tcfg.is_binary(m) => {
278+
Ok(relpath) if !tcfg.is_binary(m) && !collision_modules.contains(m) => {
189279
// The module is inside the build directory, use nested modules
190-
let mut cur = &mut module_tree;
191-
for sm in relpath.iter() {
192-
let path = Path::new(sm);
193-
let name = get_module_name(path, true, false, false).unwrap();
194-
cur = cur.0.entry(name).or_default();
195-
}
280+
module_tree.insert_nested(module_path(relpath));
281+
}
282+
Ok(relpath) if !tcfg.is_binary(m) => {
283+
collision_relpaths.push(relpath.to_path_buf());
196284
}
197285
_ => {
198286
let relpath = diff_paths(m, build_dir).unwrap();
@@ -207,10 +295,28 @@ fn convert_module_list(
207295
}
208296
}
209297
}
298+
for relpath in collision_relpaths {
299+
let mut path = module_path(&relpath);
300+
// Insert the shadowed file as an explicit `#[path]` module in its
301+
// parent. `insert_path_module` renames it if the stem collides with the
302+
// sibling directory (or another module), so pass the plain stem.
303+
let module_name = path.pop().unwrap();
304+
let file_name = relpath.file_name().unwrap().to_str().unwrap().to_string();
305+
module_tree.insert_path_module(&path, file_name, module_name);
306+
}
210307
module_tree.linearize(&mut res);
211308
res
212309
}
213310

311+
fn module_path(path: &Path) -> Vec<String> {
312+
path.iter()
313+
.map(|component| {
314+
let path = Path::new(component);
315+
get_module_name(path, true, false, false).unwrap()
316+
})
317+
.collect()
318+
}
319+
214320
fn emit_thin_binaries(
215321
tcfg: &TranspilerConfig,
216322
build_dir: &Path,

c2rust-transpile/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![feature(is_some_and)]
12
#![allow(clippy::too_many_arguments)]
23

34
mod diagnostics;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use std::fs;
2+
use std::path::PathBuf;
3+
use std::process::Command;
4+
5+
use c2rust_rust_tools::RustEdition;
6+
use c2rust_transpile::{ReplaceMode, TranspilerConfig};
7+
use tempfile::TempDir;
8+
9+
fn config(output_dir: PathBuf) -> TranspilerConfig {
10+
TranspilerConfig {
11+
dump_untyped_context: false,
12+
dump_typed_context: false,
13+
pretty_typed_context: false,
14+
dump_function_cfgs: false,
15+
json_function_cfgs: false,
16+
dump_cfg_liveness: false,
17+
dump_structures: false,
18+
verbose: false,
19+
debug_ast_exporter: false,
20+
emit_c_decl_map: false,
21+
incremental_relooper: true,
22+
fail_on_multiple: false,
23+
filter: None,
24+
debug_relooper_labels: false,
25+
cross_checks: false,
26+
cross_check_backend: Default::default(),
27+
cross_check_configs: Default::default(),
28+
prefix_function_names: None,
29+
translate_asm: true,
30+
use_c_loop_info: true,
31+
use_c_multiple_info: true,
32+
simplify_structures: true,
33+
panic_on_translator_failure: false,
34+
emit_modules: true,
35+
fail_on_error: true,
36+
replace_unsupported_decls: ReplaceMode::Extern,
37+
translate_valist: true,
38+
overwrite_existing: true,
39+
reduce_type_annotations: false,
40+
reorganize_definitions: false,
41+
enabled_warnings: Default::default(),
42+
emit_no_std: false,
43+
output_dir: Some(output_dir),
44+
translate_const_macros: Default::default(),
45+
translate_fn_macros: Default::default(),
46+
disable_rustfmt: false,
47+
disable_refactoring: false,
48+
preserve_unused_functions: false,
49+
log_level: log::LevelFilter::Warn,
50+
edition: RustEdition::Edition2021,
51+
deny_unsafe_op_in_unsafe_fn: false,
52+
postprocess: false,
53+
emit_build_files: true,
54+
binaries: vec!["main".to_owned()],
55+
thin_binaries: true,
56+
c2rust_dir: Some(
57+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
58+
.parent()
59+
.unwrap()
60+
.to_path_buf(),
61+
),
62+
}
63+
}
64+
65+
#[test]
66+
fn sibling_file_and_directory_modules_do_not_collide() {
67+
let td = TempDir::new().unwrap();
68+
let src = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
69+
.join("tests/module_paths/sibling_file_and_directory");
70+
let output_dir = td.path().join("translated");
71+
72+
let (_compile_commands_dir, compile_commands_path) =
73+
c2rust_transpile::create_temp_compile_commands(&[
74+
src.join("main.c"),
75+
src.join("hash.c"),
76+
src.join("hash_.c"),
77+
src.join("hash/sha1.c"),
78+
]);
79+
80+
c2rust_transpile::transpile(config(output_dir.clone()), &compile_commands_path, &["-w"]);
81+
82+
let lib_rs = fs::read_to_string(output_dir.join("lib.rs")).unwrap();
83+
assert!(
84+
lib_rs.contains("pub mod hash_;"),
85+
"generated module tree should include the non-colliding hash_.rs \
86+
module normally:\n{lib_rs}"
87+
);
88+
assert!(
89+
lib_rs.contains("#[path = \"hash.rs\"]\n pub mod hash_1;"),
90+
"generated module tree should include the sibling hash.rs module via an \
91+
explicit path module inside its parent instead of shadowing it with \
92+
the hash/ directory:\n{lib_rs}"
93+
);
94+
assert!(
95+
lib_rs.contains("pub mod sha1;"),
96+
"generated lib.rs should include the nested hash/sha1.rs module:\n{lib_rs}"
97+
);
98+
99+
let status = Command::new("cargo")
100+
.arg("build")
101+
.env("CARGO_TARGET_DIR", td.path().join("target"))
102+
.current_dir(&output_dir)
103+
.status()
104+
.unwrap();
105+
assert!(status.success(), "generated crate should build");
106+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#include "hash/sha1.h"
2+
3+
int hash_value(int x) {
4+
return sha1_mix(x) + 1;
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
int sha1_mix(int x) {
2+
return x * 3;
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
int sha1_mix(int x);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
int hash_utility(void) {
2+
return 7;
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
int hash_value(int);
2+
3+
int main(void) {
4+
return hash_value(13) == 40 ? 0 : 1;
5+
}

0 commit comments

Comments
 (0)