modules
every program so far has lived in one directory. that directory is a module: its .kso files share a single namespace, alphabetized within each file, and nothing more is needed until code wants to live somewhere else. this chapter is about the moment it does—what an import means, which names cross, and how two modules share a name without a fight.
an import enrolls names
here is a small geometry module and a program that uses it. geo/geo.kso marks two functions pub; a third is private working machinery.
import "std/math"
pub fn area r
3.14159 * square r
pub fn ride_km a b
math/sqrt (area a + area b)
fn square x
x * x
import "../geo"
print "one wheel: {area 3.0}"
print "the pair: {geo/area 2.0}"
print "a ride: {ride_km 1.0 2.0}"
one wheel: 28.27431
the pair: 12.56636
a ride: 3.963325623765981
one line declares the dependency, and every pub name arrives twice over. the bare spelling—area, ride_km—enrolls into the file's overload space, where it dispatches like any local function. the qualified spelling—geo/area—is the name's permanent identity, always valid, and it doubles as a tool: writing the qualifier narrows dispatch to that module's arms alone.
there is no list of imported names to maintain. the compiler prunes whatever the file never uses, and your editor's language server answers "where did this come from?"—provenance is tooling's job.
in a file that imports more than one module, i reach for the qualified spelling—geo/area answers where a name came from without leaving the line. save the bare spelling for the vocabulary your file is actually about.
shared names are the normal case
two modules will export the same word, and a local function may use it too. all of them land in one overload space, and dispatch picks by specificity—the same mechanism that chooses between a fact 0 arm and a fact n arm chooses between a local select on invoices and the list module's select. a local arm sorts before an imported one when the shapes tie, so your own definitions win the close calls. when two imports collide head-on—the same name, the same shape, and no local arm to break the tie—a bare call is refused, and the fix is one qualifier.
privacy holds at the boundary
inside a module, files see everything; pub only matters to outsiders. reach for private machinery across an import and the boundary answers:
import "../geo"
print "{geo/square 3.0}"
error[opacity]: `square` is private to module `geo`—only pub names cross an import
--> nosy/main.kso:3:9
3 | print "{geo/square 3.0}"
^
renaming on the way in
sometimes an imported name deserves a different one in your file—it collides with a word you want, or your domain has a better word. braces rename, and an identifier before them aliases the qualifier itself. the two combine:
import g { area:disc } "../geo"
print "renamed: {disc 2.0} and {g/disc 2.0}"
print "the alias still reaches: {g/ride_km 1.0 2.0}"
renamed: 12.56636 and 12.56636
the alias still reaches: 3.963325623765981
a rename replaces the spelling. after area:disc, this file says disc or g/disc; bare area no longer resolves here. the whole module still enrolls—the braces adjust one token, they never select a subset.
re-exports shape a surface
a module's surface is its own pub names. what it imports stays off that surface: depend on a module and you get its exports, never its suppliers'. when a module genuinely wants to pass a name through—the resort below is built on geometry and its guests will need area—it says so with a bare pub line:
import "../geo"
pub area
pub fn lap r
area r + area r
import "../resort"
print "a lap: {lap 1.0}"
print "area came through: {area 1.0}"
a lap: 6.28318
area came through: 3.14159
pub area puts one imported export on the resort's surface. pub geo would re-export the whole module by its qualifier, and pub area:footprint renames on the way out. anything not re-exported stays sealed:
import "../resort"
print "{resort/ride_km 1.0 2.0}"
error[name]: unknown name `ride_km`
--> sealed/main.kso:3:9
3 | print "{ride_km 1.0 2.0}"
^
the visitor imported the resort, and ride_km belongs to geometry—the resort never re-exported it, so it never arrived.
a surface is easiest to keep small from the start. export the two names your first caller needs and let the next caller ask for the third—adding a pub line is a one-word change, and taking one back breaks somebody.
what to remember
- a module is a directory; its files share one namespace.
import "path"declares the dependency and enrolls every pub name, bare and qualified. the compiler prunes what you don't use.- overloading is the resolution mechanism. specificity picks; a local arm outranks an import on a tie; a head-on collision between imports is an error with a one-word fix.
import g { theirs:yours } "path"aliases the qualifier and renames a token. a rename replaces the bare spelling.- a surface is a module's own pubs plus its explicit re-exports—
pub name,pub qualifier,pub theirs:yours. nothing leaks through on its own.
exercises
- make two small modules that both export a function named
areawith the same shape, import both from a third, and callareabare. read the refusal, then qualify both calls and confirm each spelling reaches its own module. - in the resort module from this chapter, re-export
areaunder a new name withpub area:footprint. confirm the visitor can callfootprintand that bareareano longer arrives. - call a private helper of an imported module by its qualified name. read the
error[opacity]diagnostic, then make the helperpuband watch the same call succeed. - geo's
ride_kmtakes two arguments. declare a localride_kmwith a different arity (in a library file beside your entry), import geo, and confirm both dispatch by argument count with no qualifier. then match the arity and work out which one answers a bare call—your own declaration outranks the import—and confirmgeo/ride_kmstill reaches the other.