RFA-631 · Case file with fixtures · Case 603 of 694 · Compiler evidence
An Out-of-Line Module Must Have One Canonical Source File
Rust supports two filesystem layouts for an out-of-line module, but not both at once. Choose one canonical file, update child paths deliberately, and make generated trees collision-safe.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- A layout migration or generator left two filesystem conventions claiming the same semantic module identity.
- First discriminating check
- Compare both candidates, choose one canonical source, and update child declarations, generated paths, and clean builds deliberately.
An out-of-line mod transport; asks Rust to load that module from the filesystem. Two conventional paths may represent it: transport.rs or transport/mod.rs. If both exist, the declaration does not contain enough information to choose. The failing fixture includes both candidates and receives E0761.
The two layouts represent the same module
The flat form keeps transport.rs beside the parent file. Its child modules can live below a transport/ directory. The older directory-root form puts the module body in transport/mod.rs. Both can express the same semantic tree.
The official E0761 explanation asks that one candidate be deleted or renamed. The Reference documents module source filenames and how paths are resolved.
I do not merge both files blindly. They may contain competing implementations, conditional code, or one stale copy left during a migration. First I compare their responsibilities and references.
Pick a layout and finish the migration
The repaired fixture uses an explicit #[path] only to make the compile fixture coexist with both candidates. In normal application code, the better repair is usually to keep one conventional canonical source and remove the ambiguity.
I generally use transport.rs for the module root and transport/http.rs for children because it avoids a directory full of mod.rs tabs. Existing projects may consistently prefer the other form. Consistency and clear ownership matter more than personal taste.
A safe migration moves code, updates mod declarations and tests, searches for path attributes and include paths, then builds every feature and target. Version control history should show one move rather than an apparent deletion and unrelated rewrite where possible.
The filesystem is not the module tree by itself
A .rs file is compiled only when reachable from the crate’s module declarations or used through an explicit path/include mechanism. Creating a directory does not automatically create a module. Conversely, inline modules can exist without separate files.
The Book explains separating modules into files. I draw the semantic module tree from mod declarations when diagnosing privacy or import behaviour, rather than inferring it only from directory shape.
Case sensitivity also matters across development environments. A path that appears to work on a case-insensitive filesystem may fail in Linux CI. I use exact Rust identifiers and include a clean checkout build in release validation.
Generated files need collision ownership
Build scripts, schema compilers, and bindgen-like tools sometimes emit module files. A hand-written transport.rs can collide with a generated transport/mod.rs after configuration changes. The generator should own a dedicated output directory under OUT_DIR, and source should include or path to one well-defined entry file.
I avoid writing generated Rust into the checked-in source tree during a normal build. It makes dirty worktrees, stale candidates, and nondeterministic module selection more likely. If generated code is checked in for review, regeneration has a separate deterministic command and CI verifies no diff.
Feature flags should not select between two physical candidates by leaving both present. One module file can use cfg to choose submodules or implementations explicitly. This keeps the identity stable while configuration changes behaviour.
Explicit path is a specialised tool
#[path = "..."] can integrate generated sources, platform-specific shims, or unusual legacy layouts. It also bypasses reader expectations and makes moves fragile. I use it only when the reason is documented and the path remains controlled.
The attribute chooses a source for that declaration, as the fixed evidence demonstrates, but it does not erase duplicate unused files from maintenance. A stale alternative can still confuse tools and future developers. Production repair includes cleaning or clearly segregating it.
Tests should start from a clean build directory because incremental state can hide module-file changes. Workspace crates may have identical module names without conflict; ambiguity is resolved relative to each declaring source file.
My E0761 checklist
- Which
mod name;declaration produced the ambiguity? - Are both
name.rsandname/mod.rspresent relative to that declaration? - Does either file contain stale or unique behaviour that must be preserved?
- Which module layout is canonical for this crate?
- Do child declarations, includes, docs, and tests follow the moved source?
- Is a generator writing into the source tree or reusing a hand-owned name?
- Could feature selection live inside one canonical module instead?
- Does a clean, case-sensitive, all-feature build confirm the final tree?
The core principle is that one module identity needs one source of truth. E0761 refuses to guess between equivalent filesystem conventions. I choose the owner, complete the migration, and keep generators and configuration from recreating the collision.