Skip to content

← Modules, Visibility, Testing and Docs step 2 of 22

Easy Primitives

Paths: absolute, relative, crate::, self::, super::

A module path tells Rust where to find a name. crate starts at the root, self starts at the current module, and super moves to its parent. Practice those movements by implementing a small path-normalization function:

pub fn canonicalize(current_module: &str, path: &str) -> Result<String, String>

This exercise rewrites strings using the rules below. It does not inspect a module tree, resolve imports or check whether an item exists or is public. The result is a normalized path, not proof that rustc would accept it.

Inputs and normalization

current_module is already normalized: "crate" for the root, or a path such as "crate::a::b". It has no whitespace, empty segments or qualifiers after the initial crate.

Split path on :: and trim whitespace around each segment. An empty path, a whitespace-only path, or any empty segment returns Err("empty path".to_string()). Check this before processing qualifiers. For example, "x::", "x::::y" and "super:: " all have this error.

After trimming, process the path from left to right:

  1. An initial crate selects the root. An initial self keeps the current module. Consume at most one of these initial qualifiers.
  2. Each following super moves up one level. Return Err("above crate root".to_string()) if a move would go above the root.
  3. If no qualifier was consumed and the first segment is std, core or alloc, treat it as an external crate name. Join all segments with :: and return that string without a crate:: prefix.
  4. Otherwise, append the remaining segments to the selected module and return its full path beginning with crate.

Apart from the empty-segment cases, inputs use plain identifier segments and qualifiers only in the positions described above. You do not need to validate identifier syntax or handle generics, aliases, macros or later qualifiers such as a::super::b.

A path may end after its qualifiers. From crate::a::b, self returns crate::a::b, super::super returns crate, and crate also returns crate.

Examples

These examples all start in crate::a::b:

Input path Result
crate::x::Y Ok("crate::x::Y")
self::helper Ok("crate::a::b::helper")
helper Ok("crate::a::b::helper")
super::sibling Ok("crate::a::sibling")
self::super::x Ok("crate::a::x")
super::super::super::x Err("above crate root")
std :: collections :: HashMap Ok("std::collections::HashMap")
self::std::thing Ok("crate::a::b::std::thing")
crate::std::thing Ok("crate::std::thing")

The table omits .to_string() around result strings for readability. The function returns owned String values for both success and failure.

How this relates to real Rust

Real paths also depend on names in scope. A local module or an import can change what a bare name refers to. The exercise deliberately gives the three external names priority and treats all other bare names as local.

Here is a complete example showing the difference. The nested module named std shadows the external crate when accessed through a bare std path. In edition 2018 or later, ::std explicitly selects the external crate.

mod outer {
    pub const VALUE: u32 = 7;

    pub mod inner {
        mod std {
            pub const VALUE: u32 = 9;
        }

        pub fn check() {
            assert_eq!(crate::outer::VALUE, 7);
            assert_eq!(super::VALUE, 7);
            assert_eq!(self::super::VALUE, 7);
            assert_eq!(self::std::VALUE, 9);
            assert_eq!(std::VALUE, 9);
            assert_eq!(::std::mem::size_of::<u32>(), 4);
        }
    }
}

fn main() {
    outer::inner::check();
}

A leading :: is valid Rust syntax, but this exercise's string format treats it as an empty first segment. Do not add a separate global-path rule.

The fixed external-name list is also an exercise rule. In a normal std build, std and core are available through the extern prelude. alloc is not automatically included; it can be brought into scope with extern crate alloc;. Other external names depend on the build and crate declarations. Having no Cargo manifest does not itself determine the extern prelude.

When a real path fails to compile, use the diagnostic to locate the problem. E0432 reports an unresolved import, E0433 a resolution failure, and E0603 an inaccessible private item. The surrounding message matters: a missing crate, a misspelling and an incorrectly qualified path can require different fixes. For a private item, look for its intended public API before changing visibility.