Paths for Referring to Items in the Module Tree

A path is a way to refer to an item in the module tree. Paths can take two forms: absolute and relative.

Absolute Paths

An absolute path starts from the crate root and uses the full path to an item:

public module restaurant {
    public module food {
        public module appetizers {
            public fn bruschetta() {
                println!("Making bruschetta")
            }
        }
    }
}

fn main() {
    // Absolute path
    restaurant.food.appetizers.bruschetta()
}

This is the most explicit and clear form. It tells readers exactly where the item comes from.

Relative Paths

A relative path starts from the current module and builds from there. You can reference items without writing the full path:

module restaurant {
    module food {
        public fn appetizer() {
            println!("Appetizer")
        }

        public fn describe() {
            // Relative path - within the same module
            appetizer()
        }
    }

    module house {
        fn greet() {
            // This won't work - food is a sibling, not parent
            // food.appetizer()  // Error!
        }
    }
}

Within a module, you can call other items in the same module directly. However, sibling modules require you to use their full name relative to a common parent.

Understanding the Module Hierarchy

Think of the module tree like a filesystem:

crate_root
├── restaurant          (module)
│   ├── food           (module)
│   │   ├── appetizers (module)
│   │   │   └── bruschetta (function)
│   │   └── mains      (module)
│   │       └── pasta  (function)
│   └── house          (module)
│       └── greet      (function)
└── main               (function)

To access an item, you navigate this tree. For example:

  • restaurant.food.appetizers.bruschetta - Start at root, go to restaurant, then food, then appetizers, then call bruschetta
  • restaurant.house.greet - Start at root, go to restaurant, then house, then call greet

Public vs. Private in Paths

Paths work only for public items. If an item or any parent module is private, you can't access it from outside:

module restaurant {                  // Private (not marked public)
    public fn greeting() { }
}

fn main() {
    restaurant.greeting()  // Error! restaurant is private
}

To fix:

public module restaurant {           // Make parent public
    public fn greeting() { }
}

fn main() {
    restaurant.greeting()  // OK
}

This is the public visibility rule: all ancestors in the path must be public for you to access an item.

Paths in Imports

When you import, you're creating a new name binding using a path:

import restaurant.food.appetizers as starters

fn main() {
    starters.bruschetta()  // starters is the imported name
}

The import statement says: "Follow the path restaurant.food.appetizers and bind the result to the name starters."

Paths with Generics and Complex Types

When dealing with generic types or trait objects, paths can include type information:

import std.collections.HashMap

fn main() {
    // HashMap is a path referring to a generic type
    let map: HashMap[String, Int] = HashMap()
}

The path std.collections.HashMap refers to the type itself, which can be instantiated with type arguments.

Documenting Paths

When you document your code, include paths in comments and doc comments:

/// Represents a restaurant's menu.
///
/// The `Restaurant` struct is found at `restaurant.Restaurant`.
/// To add items, use the `addItem` method from `restaurant.menu.Menu`.
public struct Restaurant {
    name: String,
}

This helps users understand how to navigate your module structure.

Common Path Patterns

Pattern 1: Accessing Sibling Modules

module server {
    module http {
        fn handleRequest() { }
    }

    module websocket {
        fn handleConnection() {
            // To call sibling module, use full path from parent
            http.handleRequest()  // This won't work!
            // Instead:
        }
    }
}

From within websocket, you need to reference http as a sibling. The safest approach is to use the full path:

module server {
    module http {
        public fn handleRequest() { }
    }

    module websocket {
        fn handleConnection() {
            // Use the full path (but this requires http to be public)
            server.http.handleRequest()
        }
    }
}

Or more simply, within the same crate, you can use the parent module:

public module server {
    public module http {
        public fn handleRequest() { }
    }

    public module websocket {
        fn handleConnection() {
            // Within the same public parent, access via dot notation
            http.handleRequest()  // This works within the server module
        }
    }
}

Pattern 2: Accessing Parent Items

public module restaurant {
    public module kitchen {
        fn cook() {
            // You can't easily reference parent items
            // Instead, structure code to avoid this need
        }
    }

    public fn announceReady() {
        println!("Food is ready!")
    }
}

fn main() {
    // You access via the full path
    restaurant.announceReady()
}

If you need parent functionality in a child module, consider:

  1. Passing it as a parameter
  2. Creating a shared utility module
  3. Restructuring to avoid the parent dependency

Pattern 3: Items in the Same Module

public module restaurant {
    public fn appetizer() {
        println!("Appetizer")
    }

    public fn mainCourse() {
        // Within the same module, call directly
        appetizer()  // This works
    }
}

Re-exports and Path Visibility

When you re-export an item, you create an alternative path to it:

// src/lib.ox
external module internals

public import internals.helpers.createMessage

// Now there are two paths to the same item:
// 1. internals.helpers.createMessage (private, internal only)
// 2. createMessage (public, preferred)

Users see the shorter path, while your internal structure remains hidden.

Full Paths in Error Messages

When the compiler reports an error, it uses paths to tell you about items:

error: cannot find function `bruschetta` in module `restaurant::food::appetizers`
 --> main.ox:3:5
  |
3 |     bruschetta()
  |     ^^^^^^^^^^ not found in this scope
  |
help: consider using the full path with the item's type
  |
3 |     restaurant.food.appetizers.bruschetta()
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The compiler shows the full path and suggests how to fix it.

Comparison with Rust

In Rust:

  • Absolute paths start with crate::
  • Relative paths use self:: or super::
  • Paths use :: not .
  • You can use super to reference parent modules

Rust example:

#![allow(unused)]
fn main() {
// Absolute path
crate::restaurant::food::appetizers::bruschetta();

// Relative path with super
super::other_module::do_something();

use crate::restaurant::food::appetizers as starters;
}

Oxide equivalent:

// Absolute path
restaurant.food.appetizers.bruschetta()

// Relative path (within same module)
otherModule.doSomething()

import restaurant.food.appetizers as starters

Oxide's simpler path syntax (no ::, crate::, or super::) makes navigation more intuitive, especially for those familiar with object-oriented languages.

Summary

  • Paths refer to items in the module tree using dot notation
  • Absolute paths start from the crate root
  • Relative paths navigate from the current module
  • Privacy rules: all ancestors in a path must be public
  • Paths are clear: they explicitly show where items come from
  • Importing creates shorter names for paths
  • Re-exports create alternative paths to the same items
  • Oxide uses simple dot notation, unlike Rust's :: syntax

Now that you understand paths, let's look at practical file organization strategies.