A program outgrows one file the day two ideas stop wanting to share a
screen. What happens next separates languages more than most features do:
C shares everything through headers that repeat declarations by hand; Java
makes the unit a class whether or not a class is the idea; Go makes
visibility a spelling convention. Mica’s answer is Pascal’s, sharpened: a
compilation unit whose surface is declared — exp marks what escapes,
everything else is private — and an import doctrine with one rule and no
special cases.
The example is Units: three files, one dependency order, the smallest complete build.
An object unit and its surface
object geometry;
imp
SquareRoot = Sqrt : cstd;
type
exp Point = record
X : float64;
Y : float64;
end;
function SquareOf(v : float64) : float64; { private — no 'exp' }
begin
SquareOf := v * v;
end;
exp function Distance(a : Point, b : Point) : float64;
begin
Distance := SquareRoot(SquareOf(b.X - a.X) + SquareOf(b.Y - a.Y));
end;An object unit is a compilation unit without a program entry:
declarations, a surface, and a closing empty compound statement. The
surface is Point, Distance, Midpoint — and not SquareOf, however
useful it looks. Try to import it and the compiler explains the whole
model in one sentence:
analyzer error 5021: no declarations found in namespace 'geometry' for
identifier: SquareOf — a library exports only its 'exp'-marked
declarations, so a name that exists in its source may still be privateA private helper is not a convention; it is unreachable. The unit’s inside stays free to change without a single importer noticing.
One import rule, no special cases
imp
WriteLn : std;
Point, Distance, Midpoint : geometry;
PathLength : statistics;geometry is a namespace exactly like std or strings — the import
doctrine is one rule: everything arrives by imp, whether written by
the Mica project years ago or by you, this morning, in the file beside this
one. Nothing is ambient; every name in a file traces to a declaration or an
import line at the top. The as rename (Sqrt as SquareRoot) is part of
the same rule — the importer chooses its local vocabulary.
Surfaces compose: statistics imports Point from geometry and speaks it
in its own exported signature, so the program above it receives a type that
traveled through two units:
Units — a program grown past one file
From geometry
home to shop: 5.000000
midpoint of that walk: (1.500000, 2.000000)
From statistics
home via shop to park: 11.000000The build is a list, the order is a discipline
MICA_SOURCE_FILES := geometry.mica statistics.mica Units.mica
The compiler accepts the sources in any order — it resolves the imports itself. The discipline of listing each unit before its importers is for the reader: a build whose list is a valid dependency order can be read top to bottom, each file naming only files already understood. Every multi-unit program on this site keeps that discipline, and none of them needs to.
The mica.project beside the sources states the same list, so
the editor’s diagnostics are the build’s — same
units, same surfaces, while you type.
The third road: a library, and its emitted contract
Inside one build, units see each other’s surfaces directly. The moment a library is built separately — its own archive, consumed by programs it knows nothing about — the surface must travel as a file. Mica’s rule: the compiler emits it, nobody writes it.
The Utilities library builds with two switches:
MICA_EXTRA_FLAGS := --output-modifier archive --emit-contract
and the build directory then holds the archive and mica.external — the
import surface of every unit in it, written by the compiler from the exp
marks. A consumer names that contract in its own project file and imports
as if the library were a sibling:
{
"sources": ["Consumer.mica"],
"contracts": ["../build/Utilities"]
}The compiler’s gated corpus holds such pairings on every merge, and Utilities’ other consumer — a plain C program — stays in the repository as the boundary’s own proof.
No header to repeat, no binding file to maintain, no drift possible between what a library exports and what its consumers see — the contract is compiler output, regenerated with the archive it describes.
The unit lifecycle: who runs, and when
The closing begin end. block answers differently for the two unit kinds,
and the difference is the deployment story:
- An
objectunit’s body must be empty — module initialization there is reserved for a future release, and the parser says so:2038: only an empty compound statement is supported in libraries and objects, reserved for future use(the “libraries” in that sentence are same-build library targets; the separately built kind below has its own rule). - A separately built
libraryunit’s body runs — real initialization, executed before the consuming program’s own body, establishing the library’s static state. And inside that block,defer until programregisters a program-lifetime finalizer: its statement runs once at process exit, after the program’s body, in last-in-first-out registration order, reading the unit’s statics as the program last left them — not as initialization first set them.
The compiler’s own test corpus pins the whole shape; its output is the lifecycle in four lines:
main start count=10 { the library's body ran first: count is 10 }
main end count=13 { the program mutated the library's static }
fin-second { registered last, runs first — LIFO }
fin-first count=13 { reads the static as the program LEFT it }Acquisition in the body, release at exit, both beside the state they manage — the same shape defer gives an activation, lifted to the library’s whole lifetime.
What this does not do
- Signature-private types are legal, and they mean something. A unit may export a function whose result type it keeps private; importers hold such a value without naming its parts — the opaque-handle idiom, chosen rather than forbidden.
- No cyclic imports. Two units that need each other are one idea split wrongly; the dependency order the site’s examples keep visible is the design tool for finding the right split.
- No transitive re-export. Importing
statisticsdoes not hand yougeometry— every file’s imports name exactly what that file uses. - Object-unit initialization is reserved, not missing. The lifecycle section above is the current whole truth: object bodies stay empty, library bodies run, finalizers drain LIFO at exit.
Try it
git clone https://gitlab.com/mica-lang/mica-container.git
make -C mica-container/examples/Units runAdd imp SquareOf : geometry; to Units.mica and meet refusal 5021. Then
mark SquareOf with exp, watch the import succeed — and notice you just
made a promise to every future consumer that the helper will keep existing.
exp is an API decision; the refusal is the compiler asking whether you
meant to make one.
Next
Generics composes with everything here — an exported
generic function is a surface too. The series continues with the tour’s
re-homed lessons: maps, bit records, and the leave family.