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
| Modifier | Meaning |
|---|---|
(none) / public | Accessible to any module that imports this one. This is the default for top-level items. |
private | Accessible 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
privatedeclaration stays unreachable through any number of hops. Naming one in a brace list isE0241, 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
exportmay name something the exporting module itself obtained through anexport, so a chain can be as long as you like — but a module that writes noexportre-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:
- the locals and generic parameters in scope at the use;
- 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);
- 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.
namespace app::module;Every source file opens with one: its position in the module hierarchy. The compiler uses it to resolve imports and to mangle symbol names, so two parse functions in different modules never collide at link time.
namespace std::collections::array;Array<T, A>: growable, heap-backed, contiguous sequence.
module pagepublic function name() { ... }
public:Visible to any module that imports this one - the default for top-level items and for struct fields, so it is mostly written to be explicit. public: opens a visibility block that groups the members after it.
public module collections::array;In a directory's _module.cryo aggregator, lists a submodule and whether it is public. Importing the directory's namespace sees only the modules declared public here.
namespace std::collections::raw_buffer;RawBuffer: the allocator-backed storage half of a growable container.
module pagenamespace std::collections::hashmap;HashMap<K, V, A>: key-value map with amortized O(1) operations.
module pagenamespace std::collections::hashset;HashSet<T, A>: unordered collection of unique values.
module pageimport a::b;
import a::b::{ X, Y };
import a::b as c;
import a::b::*;Brings a module, or selected items from it, into scope. One path per declaration: use the brace form for several items, as to alias, and * for everything public (prefer the brace form; wildcards invite name collisions).
value as TypeExplicit conversion: between numeric types, or reinterpreting one pointer type as another. Cryo never converts implicitly, and as inserts no range checks - a narrowing cast is the programmer's responsibility.
const NAME: Type = value;At module level, a true compile-time constant, conventionally SCREAMING_SNAKE_CASE. (mut at module level declares mutable global state instead.)
const n: i32const m: i32type struct Name { ... }
type enum Name { ... }
type trait Name { ... }
type union Name { ... }Begins a type declaration; the keyword that follows says which kind. On its own, type Name = Existing; declares an alias.
type struct Name {
field: Type;
method(&this) -> Ret { ... }
}A value type with named fields and optional methods. Structs live on the stack and are passed by value. Fields are public by default and may carry = default values; methods take their receiver as &this, mut &this, or this.
type struct TagTag.n: i32type struct LocLoc.n: i32function parse(text: Str) -> Result<JsonValue, JsonError>Parse a complete JSON document. Returns the parsed value on success, or a JsonError pointing at the first byte that failed to parse. Trailing whitespace is allowed; trailing non-whitespace produces JsonErrorKind::TrailingData.
const b: i32const t: Tagtype struct Tagprivate type struct Name { ... }
private:On a field: only the declaring type's own methods may touch it (E0353). On a top-level item: confined to its own module, and naming a private type elsewhere is E0503. private: opens a block of such members.
type struct Cursorfunction name<T>(param: Type) -> Ret { ... }Declares a function. Parameters are written name: Type and are never inferred; the return type follows -> and defaults to void. Declaration order does not matter: every signature is collected before any body is checked, so functions may call each other freely and recurse.
public function scan(...) -> implement Iterator<i32>function name(count: i32, args...)A variadic parameter: the trailing bucket receives any number of further arguments with C's calling convention, so the function stays ABI-compatible with the printf family. Wrap the bucket in VaArgs to read typed values from it.
-> implement Trait<T>An opaque type: some one concrete type that implements the trait, without naming it. The compiler infers the real type and uses it everywhere; callers see only the trait's methods. How the stdlib returns iterators without exposing their engines.
language referencetype trait Iterator {
type Item;
next(mut &this) -> Option<This::Item>;
count(mut &this) -> u64;
fold<Acc>(mut &this, initial: Acc, f: (Acc, This::Item) -> Acc) -> Acc;
for_each(mut &this, f: (This::Item) -> void) -> void;
take(this, n: u64) -> TakeIter<This>;
skip(this, n: u64) -> SkipIter<This>;
map<B>(this, f: (This::Item) -> B) -> MapIter<This, B>;
filter(this, pred: (&This::Item) -> boolean) -> FilterIter<This>;
chain<J>(this, other: J) -> ChainIter<This, J>;
// … 7 more
}return value;Leaves the current function with the given value. A function whose return type is not void must return one; a bare return; is for void. In an async function it completes the future with the value.
type struct Cursorfor (mut i: i32 = 0; i < n; i++) { ... }
for (item in iterable) { ... }Two forms. The C-style for (init; condition; step) scopes its loop variable to the body. for (x in expr) iterates anything with next() -> Option<T>: an iterator, a range such as 0..n, an Array or Slice (their iter() is inserted for you), or a fixed-size array.
x: i32for (item in iterable) { ... }Separates the loop variable from the sequence in a for loop. The sequence is evaluated once; the loop lowers to loop { match (it.next()) { Some(x) => .., None => break } }.
public function scan(...) -> implement Iterator<i32>mut name: Type = value;A mutable binding, reassignable after declaration - mutability is opt-in, const is the default. At module level it declares mutable global state. Before & it marks an exclusive reference or receiver instead.
mut c: Cursornamespace std::future;std::future: stackless async foundations — Future, Poll, Context, Waker, and the drivers that poll them.
// reservedListed among the declaration keywords, but the reference defines no export form today: top-level items are public by default, and private is what narrows them.
type struct BufferBuffer.handle: u64type struct VertexVertex.v: i32function main() -> i32const name: Type = value;An immutable binding: it cannot be reassigned after initialisation. The annotation may be omitted when an initialiser is present - the binding takes the initialiser's concrete type, inferred locally. Mutability is opt-in through mut.
const o: Optionconst twice: i32const twice: i32type struct Array<T, A = GlobalAlloc> where A: Allocator {
ptr: T*;
length: i64;
capacity: i64;
alloc: A;
}Array<T, A>: growable, heap-backed, contiguous sequence.
std::collections::arraystart..endA half-open range - start up to but excluding end - and sugar for Range::new(start, end): an iterator usable in for, and an ordinary value anywhere else. It binds looser than arithmetic, so a..b + 1 is a..(b + 1).
type union Name {
i: i64;
f: f64;
}An untagged, C-style union: every field shares the same storage at offset 0, so the size is that of the largest member. Writing one field and reading another reinterprets the bytes; there is no runtime tag.
language referencetype enum Name {
Unit,
Payload(T),
Named { x: i32 },
}An algebraic data type. Variants may be bare or carry a tuple or struct payload, and a match on an enum must cover every variant. Name::Variant names a variant; methods are added with implement enum Name { ... }.
type struct Context {
waker: Waker;
}The context threaded through a poll call. Holds the Waker a future registers when it returns Pending. Passed by pointer down the poll tree, matching the stdlib convention of moving aggregates by pointer.
type struct Waker {
data: u8*;
}A handle a stalled future stores so it can be re-scheduled: wake_fn(data) tells the executor "poll my task again". This is the manual-vtable shape used throughout the runtime — non-capturing function pointers plus an opaque data pointer identifying the task.
type enum Poll<T> {
Ready(T);
Pending;
}The result of polling a Future: either the final value, or a signal that the value is not ready yet and the task should be re-polled after its Waker fires.
type struct Bufferfunction pow(base: f64, exponent: f64) -> f64