Modules and Packages
We have used mod, use, pub, and prelude.ar informally throughout the book. This chapter pulls them together: how a package is laid out, what the visibility tiers actually mean, and how the conventions compose.
Files are modules
Every .ar file under src/ is a module. The file path under src/, with / replaced by ::, is the module’s qualified path:
src/prelude.ar → lease_tutorial::prelude
src/party.ar → lease_tutorial::party
src/lease.ar → lease_tutorial::lease
src/legal/california.ar → lease_tutorial::legal::california
src/legal/mod.ar → lease_tutorial::legal
Two file-name conventions carry weight:
prelude.aris the package’s library entry. It is also aliased aspkg::prelude, so consumersuse lease_tutorial::prelude::*to bring in the whole curated surface in one line.mod.aris a directory module’s primary file. When you havesrc/legal/california.arand want to add additional content to thelegalmodule itself (rather thanlegal::california), put it insrc/legal/mod.ar. Withoutmod.ar,src/legal/is a namespace only — you canuse lease_tutorial::legal::californiabutlease_tutorial::legalitself has no items.
A third file role: root.ar is the project entry — declared in [project] entry, it is the file the toolchain starts module discovery from. Both library packages (which expose a curated surface through prelude.ar) and application packages have a root.ar; the file’s job is to use foo::* every internal submodule so the project’s full module graph is reachable from one anchor.
Inline mod
A module can also be declared inline:
mod legal {
pub kind Statute { ... }
mod federal {
pub kind FederalLaw : Statute { ... }
}
}
Inline modules nest. The path for FederalLaw is lease_tutorial::legal::federal::FederalLaw. Use inline mod when the sub-module is small enough that splitting into its own file would be overkill. Use a separate file when the sub-module has substantial content.
Variants: _catalog/mod-inline/{minimal,nested-mod,mod-with-pub-items,cross-package,negative-bad-shape}/ — the cross-package/ variant exercises inline-mod items re-exported across a workspace boundary; the nested-mod/ variant demonstrates the qualified-path resolution shown above.
A worked excerpt from _catalog/mod-inline/nested-mod/src/prelude.ar:
use metatypes::*
mod jurisdiction {
pub kind Statute {
cite: String,
text: String,
}
mod federal {
pub kind FederalLaw <: Statute {
uscode_section: String,
}
}
mod state {
pub kind StateLaw <: Statute {
state_code: String,
}
}
}
// Qualified-path resolution works across inline mods:
use jurisdiction::federal::FederalLaw
use jurisdiction::state::StateLaw
Inline modules nest as a tree; each level adds one segment to the qualified path. The compiler treats jurisdiction::federal exactly as it would a src/jurisdiction/federal.ar file.
use
Three import shapes:
use ufo::prelude::* // glob — every exported item from the path
use ufo::a::endurants::{Object, Person} // named — specific items
use ufo::a::endurants::{Object as Thing} // named with rename
use lease // module — the module itself, not its items
The as rename form is the canonical fix when two upstream packages export the same name and you need both in scope. Variants: _catalog/use-rename/{minimal,cross-package-rename,composition-with-re-export,negative-name-collision,idiom}/.
// continuing the import patterns above
use produce::Apple
use tech::{Apple as Computer} // disambiguates the collision
Choose by intent. Glob imports are convenient for prelude-style modules whose items you almost always want; named imports are explicit about what you depend on; module imports let you write lease::Lease when you want the qualifying segment to appear at the call site.
pub use re-exports
pub use is the canonical pattern for curating a public surface:
// in src/prelude.ar
pub use metatypes::*
pub use party::*
pub use lease::*
Each pub use ::* brings the named module’s exported items into prelude and re-exports them. A consumer who writes use lease_tutorial::prelude::* sees everything, without knowing or caring which internal module declared what.
The pattern decouples your public API from your internal organization. You can split lease.ar into three files later — core.ar, subtypes.ar, lifecycle.ar — and update prelude.ar to re-export from them; no consumer code changes.
Two-tier visibility
There are exactly two visibility levels:
| Visibility | Reach | Default? |
|---|---|---|
pub | Cross-module (and cross-package, when re-exported) | No — opt-in |
| Module-internal | File-scoped only | Yes |
There is no third tier. There is no per-package or per-workspace pub(crate) equivalent. The two-tier discipline is the design choice — when you want a wider surface, re-export through prelude.ar; when you want a narrower one, leave items unmarked.
The default direction matters: an Argon item is file-scoped until you export it. This is the reverse of OOP-conventional public-by-default. Opting into export is explicit.
The direct-dependencies rule
A package can use only items reachable through its own [dependencies], not items reachable transitively through some dependency’s dependencies.
If your package imports cofris and cofris itself imports coex, your package cannot use coex::Foo unless you also list coex in your own [dependencies]. The compiler rejects the transitive import; the rule is not a warning.
The intent is clarity: a package’s manifest is a contract over what it depends on. Transitive use creates accidental coupling — when an upstream removes a dep, downstreams break unexpectedly. The direct-dependencies rule eliminates the surprise.
Putting the conventions together
A typical small package:
my-domain/
├── ox.toml # package manifest
├── ox.lock # lockfile (generated)
└── src/
├── prelude.ar # pub use re-exports — the curated surface
├── core.ar # central concept declarations
├── rules.ar # derives + asserts
├── queries.ar # named queries
├── computes.ar # pure functions
├── mutations.ar # state-changing operations
└── tests.ar # test blocks
prelude.ar re-exports from each file. Internal modules stay private-by-default; only items they pub slip through prelude’s globs.
A larger package adds directories:
my-large-domain/
├── ox.toml
├── ox.lock
└── src/
├── prelude.ar
├── party/
│ ├── mod.ar
│ ├── person.ar
│ ├── org.ar
│ └── relationship.ar
└── lease/
├── mod.ar
├── lease.ar
├── lifecycle.ar
└── subtypes.ar
Each directory’s mod.ar re-exports from its siblings. The package’s prelude.ar re-exports from each top-level directory. Consumers use my_large_domain::prelude::* and the whole curated surface lands.
Edge cases worth knowing
- Module names canonicalize.
my-packagebecomesmy_packageforusepaths. Hyphens in package names are friendly for the registry; underscores are the canonical form in code. - No item renames at the
pub usesite. You can rename at theusesite (use foo::{Bar as Baz}); you cannot at thepub usesite. Rename via a wrapper module if you need this. - Glob re-exports do not propagate beyond direct re-export. If
pkg-A’s prelude ispub use pkg-B::*andpkg-B’s prelude ispub use pkg-C::*, thenpkg-A’s prelude exposes everything frompkg-B’s prelude including its re-exports ofpkg-C. The fixpoint is computed at elaboration; chain re-exports work as expected. pubdoes not mean “registered globally.” Visibility is per-package; the registry is a separate concern.
Summary
Files are modules; mod declares inline modules; use imports; pub opts items into the cross-module surface; pub use re-exports through the prelude. The default visibility is module-internal; the two-tier system is the discipline. The direct-dependencies rule keeps imports honest. The lease tutorial uses these conventions throughout.