Modules & Imports
Nomi has three layers of organization. A file is a single .nomi
source file — what imports point at, and the unit pub operates on
(visibility is file-level). A module is a named API container
declared inside a file with module, such as IO or Math: it groups names
and can nest, but it is not a value and cannot implement interfaces. A
package is a directory tree rooted at nomi.toml — what ships together
and what the orphan rule operates on. Today package experiments also use a
sibling go.mod (more on that below), but that tooling layer is still
settling. Each
file imports the names it needs explicitly; the compiler resolves the
whole program in one pass, so imports never need to be ordered or
untangled.
Import syntax
Section titled “Import syntax”Single imports stand on their own; two or more imports merge into one block. You can select several exported names from one file, select children from an owner, or alias an imported item to dodge a collision:
import {
std/calendar: Date.parse
std/io: IO
}
import std/iter: Iter as Seq
fn main() {
parsed = parse("2026-05-27")
IO.print(String.length("hello"))
IO.print(Seq.count([1, 2, 3]))
IO.print(Seq.to_string(["a", "b", "c", "d"]))
IO.inspect(parsed)
}
std/io: IO— import theIOmodule from thestd/iofile; reach members asIO.print.{ … }— block form merges several imports; whatfmtproduces when two single imports stand next to each other.Date.parse— import a child function fromDateas bareparse.as Seq— rename an imported item to dodge a local name conflict.
To select children from an owner, write the owner before .{...}:
import shape: Shape.{Circle, Rectangle}
That binds Circle and Rectangle from the exported Shape enum. Add self
when you also want the owner itself:
import shape: Shape.{self, Circle, Rectangle}
That binds Shape, Circle, and Rectangle. The self marker always needs
the explicit owner before .{...}; it cannot bind an import path like shape
or std/calendar.
Files, modules, and packages
Section titled “Files, modules, and packages”For a single-file program (everything in the tour so far) you don’t need
anything more. A larger project is a package: a directory with a
nomi.toml and one or more .nomi files inside. Files within the package
import each other by their bare file name — no path prefix.
The next example is a small package with two .nomi files and a nomi.toml.
In a real project, these files would sit next to each other in the same
directory.
// FILE: math.nomi
pub module Math
pub fn double(n: Int): Int {
n * 2
}
// FILE: main.nomi
import {
std/io: IO
math: Math
}
fn main() {
IO.print(Math.double(7))
}
// FILE: nomi.toml
[package]
name = "myproject"
entry_points = ["main"]
entry_points lists the files whose fn main is allowed to be the
program’s entry. An internal/ subdirectory creates an access barrier —
files inside internal/ can only be imported from elsewhere in the same
package.
Visibility
Section titled “Visibility”Top-level declarations are private by default. Private means visible only
inside the declaring file. To make a declaration reachable from another file,
mark it pub:
// FILE: math.nomi
pub module Math
pub fn double(n: Int): Int {
n * 2
}
// Private: only math.nomi can call this directly.
fn helper(n: Int): Int {
n + 1
}
// FILE: main.nomi
import {
std/io: IO
math: Math
}
fn main() {
IO.print(Math.double(7))
// Uncomment to see the file boundary in action — main.nomi can't
// see math.nomi's private `helper`:
// IO.print(Math.helper(7))
}
// FILE: nomi.toml
[package]
name = "vis_demo"
entry_points = ["main"]
pub works on every kind of declaration — pub fn,
pub struct, pub enum, pub interface, pub type, pub once.
A file can also start with a module header:
pub module Greeter import std/io: IO pub fn greet(name: String): Unit { IO.print("hello, ${name}") }
Everything after the header is qualified by that module from other
files (Greeter.greet("Nomi")), while declarations inside the file still refer
to siblings bare. Use a module header when a file exposes an importable API.
Executable entry files are listed in entry_points and define a top-level
fn main() callback; main is the program entry point, not part of the file’s
imported API surface.
Module blocks can nest when an API has a natural sub-surface. Each segment
you import through is part of the public API, so every crossed module needs
its own pub:
// FILE: api.nomi
pub module HTTP {
pub module Header {
pub fn canonical_name(name: String): String {
name
}
}
}
// FILE: main.nomi
import {
std/io: IO
api: HTTP.Header
}
fn main() {
IO.print(Header.canonical_name("content-type"))
}
// FILE: nomi.toml
[package]
name = "nested_module_demo"
entry_points = ["main"]
The rule is about the file boundary, not the package boundary: two files
in the same package are still different visibility units. The package (the
nomi.toml-rooted directory tree) is a separate layer — the orphan-rule and
distribution unit, not the visibility unit.
Interface implementation functions don’t take a pub of their own. Their
visibility comes from the interface they implement:
- If the interface is
pub, other files can call the implementation functions. - If the interface is private, the implementation functions stay private too.
- The rule applies to every
impl Interface for Type { ... }block.
Inherent type functions in type bodies use per-item pub, just like other
functions. They belong in the same file as the type they qualify; use
impl Interface for Type for cross-file package-level interface
implementations, not extension-style inherent APIs.
Modules versus types
Section titled “Modules versus types”Use a module when the name is only organization:
module Console { fn writeln(msg: String): Unit { // ... } }
Callers write Console.writeln("hi"), but there is no Console value to pass,
store, or use as an app field. A module may contain types, interfaces, nested
modules, functions, and impl ... for ... blocks, but the module itself is not
an implementing type.
Use a zero-data type when the name should also denote a value type:
interface Logger { fn log(value: self, msg: String): Unit } type Console impl Logger for Console { fn log(console: Console, msg: String): Unit { // ... } }
Now Console is a stateless value type. It can implement Logger, appear in
fields, be passed to functions, and be used wherever a Logger is expected.
That value-ness is the difference: modules provide hierarchy; types provide
nominal values and interface conformance.
Opaque types
Section titled “Opaque types”A pub declaration exposes both the type’s name and its
representation — its fields (struct), variants (enum), or wrapped
inner type (distinct type). For an abstract data type whose
representation you want to protect — invariants the constructor enforces,
a representation you might change later — opaque publishes the
name while keeping the construction surface (constructor, unwrap,
destructuring, and field access) private:
// FILE: ids.nomi
pub opaque type UserId Int {
pub fn new(n: Int): Result<UserId, String> {
if n > 0 {
Ok(UserId(n))
} else {
Err("user IDs must be positive")
}
}
pub fn value(id: UserId): Int {
UserId(n) = id // only ids.nomi can destructure UserId
n
}
}
// FILE: main.nomi
import {
std/io: IO
ids: UserId
}
fn main() {
case UserId.new(42) {
Ok(id) -> IO.print(UserId.value(id))
Err(e) -> IO.print("error: ${e}")
}
case UserId.new(-1) {
Ok(_) -> IO.print("unexpected")
Err(e) -> IO.print("error: ${e}")
}
// Uncomment to see the opaque barrier — main.nomi can hold a
// UserId, but can't construct or destructure one directly:
// bad = UserId(7)
}
// FILE: nomi.toml
[package]
name = "ids_demo"
entry_points = ["main"]
From main.nomi, UserId is a type you can hold, pass, and store
in collections — but you can’t construct one (UserId(7) is
rejected), destructure one (UserId(raw) = id errors), or read the
inner Int directly. The only way in and out is through UserId’s
pub functions. The validation in UserId.new becomes a real
invariant: no UserId value can exist with a non-positive inner.
opaque composes with each of the three type-declaration keywords:
pub opaque struct— hides the field list.pub opaque enum— hides the variants.pub opaque type Name InnerType— hides the wrapped representation (the form shown above).
opaque is rejected on declarations without a representation —
functions, type aliases, interfaces, once bindings, and
zero-sized type markers — since there’s nothing to hide.
Re-exports
Section titled “Re-exports”A file can re-publish names it imports — promoting them into its own
public surface — with the export modifier on the import. That’s how a
facade file curates an API out of sibling files or dependencies
without making consumers chase the underlying source:
// Re-export an imported module — consumers of this file see `Parser`. import other/parser: Parser export // Re-export an imported module under a different name. import other/parser: Parser export as ParserLib // Per-item — `add` re-exported under its own name; `subtract` // re-exported under the alias `minus`; `helper` stays private. import calc: {add export, subtract export as minus, helper} // Line-level shorthand — every selected name re-exported. import std/io: IO.{self, IOError} export
Re-exports are explicit by item — there’s no import path.* glob —
so an upstream dependency adding new public names never silently
expands your file’s API. The name being re-exported must already be
pub upstream; re-exporting a private name is an error.
export as Pub applies only to imported names, not to your file’s
own declarations. To expose a local definition under a different
name, rename the declaration itself or write a thin wrapper.
Packages Today
Section titled “Packages Today”Package management is one of Nomi’s younger layers. The language-level idea is
stable enough to use: a package is a directory rooted at nomi.toml, with files
inside forming the import graph. The current implementation also puts a
go.mod next to nomi.toml and leans on Go tooling for dependency
experiments. That is current reality, not a promise that Nomi package
management is forever Go module management.
Creating a package today uses the standard Go pair:
mkdir myproject
cd myproject
go mod init myproject
# then add nomi.toml + your .nomi files
After that, nomi.toml describes the Nomi package and go.mod carries the
current dependency mechanics. For local and Git-hosted package experiments, use
Go’s familiar tooling — go get github.com/user/somelib, then import files
from somelib in your Nomi code. The long-term ergonomics are still an open
design area.
The terminology overlap is the only awkward part: Go calls the directory tree a
“module,” while Nomi calls that layer a package. In Nomi, a file is the
import source and visibility boundary, and a module is a named API
container declared inside a file. When the tour says “Go module,” it means the
implementation/tooling artifact in go.mod, not Nomi’s module declaration.
Import order doesn’t matter
Section titled “Import order doesn’t matter”The compiler resolves names whole-program in two phases, so a module can refer to a name declared in any other module without forward declarations or careful ordering. No header files, no “include guard” ceremony. (As a side effect, cyclic imports between modules aren’t build errors either — though they’re usually a sign package boundaries want rethinking.)
The orphan rule
Section titled “The orphan rule”The flip side of the impl/interface decoupling is the orphan rule: an
impl must be declared in the package that owns either Iface or T.
That rule applies uniformly to impl Iface for T { ... } blocks, including
local, foreign, generic, and constrained-generic implementations such as
impl Iface for Box<'T> where 'T: Bound { ... }. You can’t add an
impl in your own code that bridges two third-party libraries’ types and
interfaces together — that would create coherence conflicts when both libraries
are linked. The compiler reports the violation as a build error.
The next chapter — App Fields, Resources & Context
— covers Nomi’s answer to “how do you pass runtime dependencies around without
a tangle of globals or DI containers”: app fields, scoped with rebinding,
resources, and context.