Skip to content
CryoCryo home
LanguageProgram structure

13Modules and Imports

Cryo organises code into modules using a hierarchical namespace system. Every source file declares its namespace, and files reference each other through import.

13.1 Namespaces

Every Cryo source file begins with a namespace declaration that establishes its position in the module hierarchy.

namespace MyApp;
namespace MyApp::Utils;
namespace std::collections::array;

The namespace serves as the file's identity within the project. The compiler uses it to resolve imports and to mangle symbol names so that two unrelated functions named parse from different modules do not collide at link time.

13.2 Module Aggregators

A directory of related files uses a _module.cryo aggregator to declare which submodules exist and which are public.

// stdlib/collections/_module.cryo
namespace std::collections;

public module collections::raw_buffer;
public module collections::array;
public module collections::str;
public module collections::string;
public module collections::hashmap;
public module collections::hashset;

public module is structure only: it declares that a submodule exists, drives discovery, and suppresses a build-order edge. It does not bring the child's name or the child's symbols into any importer's scope. Importing std::collections therefore does not let you name Array — reaching a submodule takes an import of that submodule, or an export in the aggregator (see section 13.5).

13.3 Imports

import Math::Vector;                  // import the module
import Math::Vector::*;               // wildcard: everything public
import Math::Vector as V;             // aliased
import Math::Vector::{ Vec2, Vec3 };  // selective import (brace list)
import Math::Vector::{ Vec2 as P2 };  // an item under another name
import Math::Vector::Vec3 as P3;      // the same, for one item

A plain import Math::Vector; binds the module and no names from it: its items are reached by path, Vector::Vec2. Only the wildcard brings a module's names in bare, and the compiler's own prelude is the one wildcard a file gets without writing it. An import whose path names no module is refused at the import (E0502), and the help names the module the path abbreviates (core::option for std::core::option).

An entry's as binds the declaration it names under another name, and only that name: import Toml::{ parse as toml_parse }; makes toml_parse Toml's parse, and does not bring parse into scope. The alias is the declaration itself - its overloads, its generic parameters, a type's statics and methods - not a second spelling looked up later.

A module alias names the module itself. import Math::Vector as V; makes V::Vec2, V::Vec2::new(..), V::length(..), V::Axis::X and the value V::length mean what the same paths through Vector mean; Math need not be a module. An alias is a module's name in its file, so an alias spelled like another module the file imports is refused (E0205). A path continues through an alias into the module's sub-modules as well as its own names:

import Json as J;

const n: i32 = J::twice(3);          // Json::twice
const m: i32 = J::Inner::deep();     // Json::Inner::deep

A plain import's name continues the same way: with import A::Toml;, Toml::Sub::three() names A::Toml::Sub::three, and the sub-module is loaded although nothing imports it.

import A::B as C; names the item B of the module A, or the module A::B. When A declares an item B and A::B is also a module, the path names both, and neither is chosen: the import is an error (E0244). The braced forms are the same error, aliased or not, so there is no import that picks the item out of the pair; the fix is to rename one of the two. The module stays reachable by its path:

import Shapes::Json as J;          // error[E0244]: `Shapes::Json` names both an item of `Shapes` and a module
import Shapes::{ Json };           // error[E0244]
import Shapes::{ Json as J };      // error[E0244]
import Shapes::Json;               // the module, as `Json::..`

A name a file declares and a name it imports are one scope. Declaring a local item with the name of an import, or with an import alias, is a redeclaration (E0205); neither the local declaration nor the import wins:

import Shapes::{ Tag };
type struct Tag { n: i32; }        // error[E0205]

import Json as Loc;
type struct Loc { n: i32; }        // error[E0205]

A library registered with cryo vendor is imported as vendor::<Name>, and that is its module's name: import vendor::RayLib; binds the module, and RayLib::InitWindow(..) reaches its items. The generated binding declares namespace RayLib;, but a module reached through a vendor:: import is named by the path the import writes.

Each import declaration imports from a single path. To bring two items from the same module into scope, use the selective brace form (import M::{A, B};) or write two separate import statements.

Wildcard imports are convenient but can cause name collisions; prefer the brace form or using the module name directly.

A brace entry brings its name in once. A second brace entry bringing in a name an earlier one already brought into the file, in the same namespace - another module's item of that name, or the same item again - is refused at the second import (E0243, "parse is imported twice"). The first import stays the name's, and a qualified path still names its own module's item:

import Json::{ parse };
import Toml::{ parse };               // error[E0243]: `parse` is imported twice
const b: i32 = Toml::parse("1");      // Toml's `parse`

The fix is to remove one of the imports, or to bind one under another name: import Toml::{ parse as toml_parse };. An alias is a name like any other, so two entries binding one alias are E0243 too. A module's overloads of one function are one entry, not a repetition.

A wildcard is not a brace entry: two wildcards offering one name make only the bare use of that name ambiguous (E0154).

Types and values are separate namespaces, for imports as for declarations. A type Tag from one module and a function Tag from another, both imported, do not collide: Tag in a type position names the type and a call Tag(..) names the function. Two imports tie only in the namespace they share, so two wildcards offering a type Tag make the type uses ambiguous (E0154) and leave a function Tag imported from a third module callable:

import Shapes::{ Tag };               // a type
import Labels::{ Tag };               // a function: no collision
const t: Tag = Tag { id: 3 };         // Shapes's type
const n: i32 = Tag(4);                // Labels's function

13.4 Visibility

ModifierMeaning
(none) / publicAccessible to any module that imports this one. This is the default for top-level items.
privateAccessible only within the declaring module - for items and members alike.

Top-level items are public by default; mark an item private to confine it to its own module. For top-level types (struct / union / enum), private is enforced across modules: naming a private type from another module - in a type annotation, a struct literal, or a function signature - is rejected with E0503. A private type remains fully usable within its own module.

This is the mechanism behind hiding iterator engines: a cursor struct can be private while its producer returns implement Iterator<...>, so callers consume the sequence through the trait and can never name the concrete type.

// engine.cryo
namespace app::engine;
private type struct Cursor { /* ... */ }          // hidden outside app::engine
public function scan(...) -> implement Iterator<i32> { return Cursor { ... }; }

// main.cryo
namespace app;
import app::engine;
for (x in scan(...)) { ... }                     // fine - never names Cursor
mut c: Cursor = ...;                             // E0503: `Cursor` is private to `app::engine`

13.5 Re-exports

An export declaration lets a module pass names it did not declare on to its own importers. This is how an aggregator presents a flat surface over a directory of files.

namespace std::future;

public module future::waker;                  // structure (section 13.2)
export future::waker::{ Context, Waker };     // re-export two names
export future::poll;                          // re-export the whole submodule

After this, a file that writes import std::future; reaches future::Context, future::Waker and future::poll's names through the facade (future::Poll), and the module poll itself (poll::Poll). A plain import binds the module and no names from it (section 13.3), so none of these is in scope bare: Context alone is refused. import std::future::{ Context, Waker }; brings those two in bare, and import std::future::*; everything the facade offers.

The rule, entire: an export grants an importer of this module exactly what the same path would grant if it were imported here — and it grants this module the same thing, so a module that re-exports a name may also use it without writing the path a second time. The forms mirror import:

export Math::Vector;                  // importers get the module, hence Vector::Vec2
export Math::Vector::{ Vec2, Vec3 };  // importers get Vec2 and Vec3, as Facade::Vec2
export Math::Vector as V;             // importers get the name V
export Math::Vector::{ Vec2 as P2 };  // importers get P2, naming Vec2

An export entry takes as as an import entry does: export std::fmt::{ printf as print }; binds print in the exporting module and offers it to its importers, where print, Facade::print and a glob all name printf itself.

There is deliberately no export M::*;. A glob re-export is the one form under which an importer's in-scope set changes because some child module gained a declaration, with the new name written down at neither end. import M::*; remains legal, where its effect is confined to the one file that wrote it.

Two rules follow and are not optional:

  • An export never widens visibility. A private declaration stays unreachable through any number of hops. Naming one in a brace list is E0241, reported where the export is written, because that is the only place an author can fix it.
  • Re-exports chain, and every hop is explicit. An export may name something the exporting module itself obtained through an export, so a chain can be as long as you like — but a module that writes no export re-exports nothing. No name arrives anywhere by accident, and moving a declaration between a module's children stays invisible to that module's ancestors.

A re-exported name is one declaration reachable by many paths: through the facade it is the same item, with the same identity, as at its home. A name that two of the facade's children both declare and both reach it through is refused where it is used through the facade (E0155), in a call as in a type annotation; neither child is picked.

Because an export changes what is reachable and never what is compiled first, two modules may re-export from each other without either becoming unsatisfiable.

13.6 Modules and Types Share One Namespace

In a file's scope a name is a module or a type, never both. A module is spelled by any whole-segment suffix of its namespace — Buffer, Gpu::Buffer, App::Gpu::Buffer for namespace App::Gpu::Buffer; — once it is imported, and by that suffix inside its own file; a type is spelled by its name once it is declared or imported. A type whose name spells a module reachable from the same scope is a redeclaration, refused at the declaration or import that completed the pair (E0205, the same error as declaring a name twice):

namespace App::Gpu::Buffer;
type struct Buffer { handle: u64; }    // E0205: `Buffer` is this module

namespace App::Main;
import App::Gpu::Vertex;
type struct Vertex { v: i32; }         // E0205: `Vertex` spells the imported module

A module may declare a type named like a different module, and a type may be reached through its module's path (Buffer::Handle) without being imported. The consequence is that a scope segment names exactly one owner, so Buffer::make is the module's function or nothing — there is no rule choosing between a module and a same-named type, because no scope holds both.

13.7 Name Lookup

A bare name is looked up through three tiers, innermost first:

  1. the locals and generic parameters in scope at the use;
  2. the file's own scope: the module's items, private ones included, and the names its imports bind - one scope, so a clash between the two is a redeclaration (E0205, section 13.3);
  3. the prelude.

An inner tier shadows an outer one silently. A local named like an imported function is not a diagnostic, and an imported type named like a prelude type replaces it in that file:

import Shapes::{ Option, twice };     // Shapes's own `Option` struct

function main() -> i32 {
    const o: Option = Option { n: 3 };   // Shapes::Option, not the prelude's
    const twice: i32 = 4;                // a local; `twice` is that local below
    return twice - 4;
}

Two candidates are ambiguous only within one tier: two wildcards offering one name (E0154), two brace entries binding it (E0243).

Nothing outside the tiers is reachable. A name declared exactly once in the whole program still does not resolve where nothing brings it into scope, and a path whose first segment no tier binds is not found, however fully it is spelled: std::math::pow(2.0, 3.0) in a file that imports no std::math is E0203.

A path resolves one segment at a time. Its first segment is a bare name, looked up as above; each later segment is looked up in what the segment before it named. A module is found only among those the file can see - its own and the ones it imports, spelled as section 13.6 allows - never by searching the program for its spelling, so what a path means never depends on which other modules exist in the program. A path that does not resolve is an error, with a suggestion where one exists.