# How Sway's Namespace and Module System Works: A Deep Dive into the Compiler Architecture

> Explore Sway's namespace and module system. Understand how packages, modules, and scopes create a hierarchical tree for symbol resolution, imports, and visibility in this deep dive into compiler architecture.

- Repository: [Fuel Labs/sway](https://github.com/FuelLabs/sway)
- Tags: deep-dive
- Published: 2026-03-04

---

**Sway's namespace system creates a hierarchical tree where Packages contain Modules, Modules contain lexical scopes, and the Namespace context threads mutable state through compilation to resolve symbols, handle imports, and enforce visibility rules.**

Sway is a smart contract programming language developed by Fuel Labs that uses a Rust-inspired module system to organize code. Understanding how Sway's namespace and module system works is essential for managing visibility, imports, and project structure in the Fuel ecosystem. The implementation resides primarily in the `sway-core` crate within the `FuelLabs/sway` repository.

## Core Components of the Sway Namespace System

The namespace architecture consists of three primary structures that work together during semantic analysis.

### Package

The **Package** represents the top-level compilation unit, analogous to a Rust crate. According to the source code in [`sway-core/src/semantic_analysis/namespace/package.rs`](https://github.com/FuelLabs/sway/blob/main/sway-core/src/semantic_analysis/namespace/package.rs), a package owns the root module and maintains a map of external dependencies. When created via `Package::new`, the package name becomes the first identifier in every absolute path.

```rust
let pkg = Package::new(Ident::new("my_contract"), None, prog_id, true);
// Path for `my_contract::utils::helpers` → [my_contract, utils, helpers]

```

### Module

The **Module** struct, defined in [`sway-core/src/semantic_analysis/namespace/module.rs`](https://github.com/FuelLabs/sway/blob/main/sway-core/src/semantic_analysis/namespace/module.rs), represents a single Sway source file. Each module tracks its absolute path (`mod_path`), a hash map of sub-modules, a stack of lexical scopes, and all declared items including functions, structs, constants, and imports. The root module's `mod_path` contains only the package name, while nested modules append their identifiers to this path.

### Namespace

The **Namespace** serves as the mutable context threaded through the type-checking phase. Implemented in [`sway-core/src/semantic_analysis/namespace/namespace.rs`](https://github.com/FuelLabs/sway/blob/main/sway-core/src/semantic_analysis/namespace/namespace.rs), it holds the current package, the absolute path of the module being processed, and helper methods for symbol resolution and import handling. The namespace transforms parsed paths into full absolute paths and manages the visibility checks required for private module access.

## Module Paths and Resolution

Sway distinguishes between absolute and relative paths during compilation, converting parsed syntax into fully qualified module paths.

### Absolute vs. Relative Paths

When the parser encounters a path like `foo::bar`, the `Namespace::parsed_path_to_full_path` method determines the path type:

- **Root-relative paths** prepend the package name
- **Submodule references** prepend the current module path
- **External package paths** remain unchanged
- **Local bindings** resolve relative to the current module

### Path Resolution Logic

The resolution implementation in [`namespace.rs`](https://github.com/FuelLabs/sway/blob/main/namespace.rs) handles the transformation:

```rust
pub fn parsed_path_to_full_path(
    &self,
    _engines: &Engines,
    parsed_path: &ModulePathBuf,
    is_relative_to_package_root: bool,
) -> ModulePathBuf {
    // 1️⃣  Root‑relative → prepend package name
    // 2️⃣  Submodule of current module → prepend current_mod_path
    // 3️⃣  External (already full) → leave unchanged
    // 4️⃣  Local binding → prepend current_mod_path again
}

```

## Entering and Leaving Sub-Modules

The compiler manages module hierarchy through explicit push and pop operations during semantic analysis.

### push_submodule and enter_submodule

When processing a `mod` declaration, the namespace invokes `push_submodule`, which internally calls `enter_submodule`. This method, defined in [`namespace.rs`](https://github.com/FuelLabs/sway/blob/main/namespace.rs), creates a new sub-module via `Module::add_new_submodule` if it doesn't exist, pushes the module name onto `current_mod_path`, and optionally imports the standard library prelude.

```rust
pub fn push_submodule(
    &mut self,
    handler: &Handler,
    engines: &Engines,
    mod_name: Ident,
    visibility: Visibility,
    module_span: Span,
    check_implicits: bool,
) -> Result<(), ErrorEmitted> {
    self.enter_submodule(handler, engines, mod_name, visibility, module_span, check_implicits)?;
    Ok(())
}

```

### pop_submodule

When the compiler finishes processing a sub-module, `pop_submodule` removes the last element from `current_mod_path`, returning the context to the parent module. This stack-based approach ensures proper lexical scoping throughout the compilation process.

## Symbol Resolution and Visibility

Sway enforces strict visibility rules during symbol lookup, checking both item-level and module-level privacy.

### resolve_symbol

The `Module::resolve_symbol` method walks the lexical scope chain from innermost to outermost, searching for the requested identifier. Each lexical scope maintains its own `Items` collection containing symbols, use statements, and glob imports. The resolution respects the `pub` keyword, ensuring private items remain inaccessible outside their defining module.

### Visibility Checks

The namespace implements additional module-level privacy through `Namespace::check_module_visibility`. If a module in the resolution path is private and the import does not originate from an ancestor module, the compiler emits an `ImportPrivateModule` error. This prevents external code from accessing implementation details hidden within private sub-modules.

## Import System

Sway supports multiple import forms, each handled by specific namespace methods that manage symbol visibility and potential name clashes.

### Item Imports

The `item_import_to_current_module` method handles explicit item imports like `use foo::Bar;`. It looks up the declared item in the source module, copies its `ResolvedDeclaration`, and registers a synonym in the destination module's `use_item_synonyms` map. This process also pulls associated trait implementations via `TraitMap::append_items_for_type`, ensuring methods remain visible.

### Star Imports

For glob imports (`use foo::*;`), `star_import_to_current_module` collects all public symbols from the source module, including re-exports, and registers them as glob synonyms. The implementation filters symbols by visibility (`src_visibility.is_public()`) and handles name clash resolution according to Sway's shadowing rules.

### Self Imports

The `self_import_to_current_module` method wraps `item_import_to_current_module` to handle aliased imports like `use foo::Bar as Baz;`. It splits the path to isolate the final identifier, then delegates to the standard item import logic while preserving the alias mapping.

## Prelude and Implicit Imports

Sway automatically imports standard library components into new modules, reducing boilerplate while maintaining explicit opt-in for external dependencies.

When `enter_submodule` creates a new module with `check_implicits = true`, the namespace invokes `import_implicits`. This method first checks for the standard library via `self.exists_as_external(&STD.to_string())`. If present, it calls `prelude_import` to bring `std::prelude::*` into the current scope.

For contract packages, the namespace additionally imports the `CONTRACT_ID` constant into non-root modules. The `import_implicits` implementation checks `self.current_package.is_contract_package()` and `self.current_mod_path.len() > 1`, then invokes `item_import_to_current_module` to register the contract identifier.

## External Packages

External dependencies integrate seamlessly into Sway's namespace hierarchy through the `Package` abstraction. The `Package::external_packages` map stores third-party libraries as distinct `Package` objects, each maintaining their own root modules and visibility boundaries.

When resolving paths that reference external packages, the namespace uses `Package::module_from_absolute_path`. This method traverses the external package's module tree using the same logic as local packages, ensuring consistent visibility and import rules across project boundaries. The compiler treats third-party code identically to local code, preserving encapsulation while enabling code reuse.

## Code Examples

### Declaring a Nested Module with Public Exports

```sway
// lib.sw
pub struct Point {
    x: u64,
    y: u64,
}

// main.sw
mod lib;          // creates submodule `lib`
pub use lib::Point; // re‑export `Point` from the root

fn main() -> u64 {
    let p = Point { x: 1, y: 2 };
    p.x + p.y
}

```

The `mod lib;` declaration triggers `Namespace::push_submodule`, creating a new `Module` with `mod_path` set to `[my_contract, lib]`. The `pub use lib::Point;` statement invokes `item_import_to_current_module`, registering a synonym in the root module's `use_item_synonyms` while preserving the `pub` visibility for downstream consumers.

### Implicit Prelude Import

```sway
// std/prelude.sw (provided by the std library)
pub fn assert(condition: bool) {
    // implementation …
}

// my_contract.sw
mod std; // external std package, automatically imported
fn test() {
    // `assert` is available without an explicit `use` because the prelude is auto‑imported.
    assert(true);
}

```

When the root module initializes with `import_std_prelude_into_root = true`, the namespace executes `prelude_import`, walking `std::prelude` and inserting all public symbols into the module's `prelude_synonyms` map.

### Star Import with Visibility Filtering

```sway
// utils.sw
pub fn foo() {}
fn private_helper() {} // not exported

// main.sw
mod utils;
use utils::*; // imports only `foo`, not `private_helper`

fn call_foo() {
    foo(); // OK
    // private_helper(); // compile‑time error: not visible
}

```

The `star_import_to_current_module` implementation filters symbols using `src_visibility.is_public()`, ensuring that only public items enter the destination module's namespace.

## Summary

- **Sway's namespace and module system** uses a three-tier hierarchy: Packages own root modules, Modules contain lexical scopes and items, and the Namespace context manages mutable state during compilation.
- **Path resolution** converts relative paths to absolute module paths via `parsed_path_to_full_path`, handling root-relative, submodule, external package, and local binding references.
- **Visibility enforcement** occurs at both the item level (via `pub` keywords) and module level (via `check_module_visibility`), preventing unauthorized access to private implementation details.
- **Import mechanisms** include item imports (`use foo::Bar`), star imports (`use foo::*`), and self imports with aliases, all of which pull associated trait implementations into the destination scope.
- **Implicit imports** automatically bring the standard library prelude and contract IDs into appropriate modules via `import_implicits` and `prelude_import`.

## Frequently Asked Questions

### How does Sway resolve module paths during compilation?

Sway resolves module paths through the `Namespace::parsed_path_to_full_path` method, which examines the path structure to determine if it is relative to the package root, a submodule of the current module, or an external package. The method prepends the appropriate base path (package name or current module path) to create an absolute `ModulePathBuf` that uniquely identifies the target module within the hierarchical namespace.

### What is the difference between item imports and star imports in Sway?

Item imports (`use foo::Bar;`) use `item_import_to_current_module` to copy a specific `ResolvedDeclaration` into the destination module's `use_item_synonyms`, while star imports (`use foo::*;`) use `star_import_to_current_module` to collect all public symbols from the source module and register them as glob synonyms. Star imports automatically filter out private items by checking `src_visibility.is_public()`, whereas item imports can target specific public or private items depending on the caller's location relative to the source.

### How does Sway handle visibility for private modules?

Sway enforces module-level privacy through `Namespace::check_module_visibility`, which verifies that an import originates from an ancestor module before allowing access to a private module in the path. If a module is marked private (the default) and an import attempts to access it from a non-ancestor context, the compiler emits an `ImportPrivateModule` error, preventing external code from depending on internal implementation details while allowing parent modules to organize sub-modules freely.

### What is the role of the prelude in Sway's namespace system?

The prelude serves as an automatic import mechanism that makes standard library utilities available without explicit `use` statements, implemented through `Namespace::import_implicits` and `prelude_import`. When a new module is entered with `check_implicits = true`, the namespace checks for the `std` external package and, if present, walks `std::prelude` to insert all public symbols into the module's `prelude_synonyms`, ensuring consistent availability of core functionality like `assert` across the project.