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

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, 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.

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, 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, 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 handles the transformation:

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, 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.

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

// 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

// 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

// 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →