16. Modules, packages, and tests
16.1 Files and modules#
One file is one module; its path is its package-relative path: src/image/color.lucb is image.color, so a file's name without .lucb is an identifier. A larger module may be a directory of that name instead, whose ORDER file lists its source fragments in order; the fragments share the one module scope, a diagnostic or a trap names the fragment file, a type declared in one fragment may take methods in another under extend (§9.5), and nothing else about the module changes. An import may stand anywhere among a module's top-level declarations, in any of its fragments, beside the declarations that use it; a module is imported once across the module (§16.3). There is no module declaration and no re-export. Module cycles are errors. Declarations are private unless pub, and a public signature may mention only public types.
16.2 The three module kinds#
The Luce runtime (§1.3) is the library that implements full Luce's reference-counted classes, its list, map, and set storage, closures, worker transfer, and boxed interface values. A module that uses any of those features is linked against it.
| Kind | Suffix | May use classes, collections, closures | May use raw pointers | Needs the Luce runtime |
|---|---|---|---|---|
| Safe Luce | .luc | yes | no | yes |
| Native Luce | .lucn | yes | yes, through audited intrinsics | yes, unless it uses none of the features above |
| Base | .lucb | no | yes, with C spelling | never |
The suffix is the kind. A safe module is ordinary full Luce. A native module is full Luce with one addition, the audited raw-pointer intrinsics. It exists so that a class in full Luce can own a Base or C resource and free it in deinit; it is the bridge between the safe tier and Base (§18.6). It is not a lower-level language: it still has classes and collections, and so it still needs the runtime. The runtime itself is written in this tier and is the one native package that needs no runtime, because it uses none of the features it implements. A Base module has no classes, collections, or closures, and so never needs the runtime.
A Base module imports Base modules and C, never a full Luce module. A safe or native module imports any kind. A package whose modules are all Base is a Base package; the sealed runtime package is one, with the single exception that it may contain .lucn modules under its own identity for its two sealed intrinsics (§18.13).
Why the direction is one-way. Everything a Base module can reach is Base or C, so a Base artifact needs no runtime, and that is checkable: the compiler's reachability analysis reports the first full Luce function a freestanding build touches.
16.3 Imports#
import image.color
import data.serialisation as serial
from image.geometry import Point
from luce_ui import ui
from luce_std import paths, files
from luce_crypto import native as crypto
from luce_geocore.core import parallelA module is named by its path under its package's source root: src/image/color.lucb is image.color. Inside the package that path is the import; another package names the module behind the package's identifier, luce_ui.ui for src/ui.lucb of luce-ui, and may import it only when that package lists it as public (§16.4). The package's own identifier may also lead its own modules' paths. A package is not a module: import luce_ui is an error, except inside luce-ui when it has a module of that name, src/luce_ui.lucb, its starter module, which import luce_ui and from luce_ui import Button there reach. A module may not take the name of another package the package imports from.
import keeps a module qualified, with an optional alias; without one the qualifier is the path's last segment, so import image.color is used as color.rgb and import luce_ui.ui as ui.Button. from pkg import a, b, where pkg is a package or a directory of modules in one (luce_geocore.core, or core of the importer's own package), imports the modules a and b as import pkg.a and import pkg.b would; it is the usual spelling for another package's modules, since the line names what the code below uses, from luce_std import files then files.read. from ... import of a module brings named declarations in and nothing else: from io import Writer brings Writer, from io import Writer, stdout brings both, and a program that also writes io.something needs import io as well. Each name a from import brings may take an alias, from luce_crypto import native as crypto, from io import Writer as Output. There are no wildcards and no relative imports. An import nothing resolves through is pruned by the checker, so nothing after it sees the import; a duplicate import is an error with an automatic fix.
16.4 Packages#
A package has a package.prisma definition and an exact lock. The definition names the package, its source root, its dependencies, its C inputs (§17.4), and the symbol_prefix for exports (§17.6). The package name is the identity of its error codes (§11.3); a name spelled luce-ui is the identifier luce_ui. There are no build scripts. A minimal definition:
#prisma 4.0
def package "demo" {
str source = "src"
str[] public = ["api"]
}Every package has one layout: its sources directly under its source root, demo/src/api.lucb, with no directory repeating the package's name, since its identifier already leads every import from outside (§16.3).
A dependency is def dependency "name" { str path = "../name" }; one without a path is a registry package that luc has unpacked under .luc/deps/<name> at or above the package root. Its public modules, the ones other packages may import, are str[] public = ["ui", "widgets.button"], by their paths under the source root, and C inputs are def native "inputs" { str[] sources = [...] } with link_search, libraries, frameworks and pkg_config alongside. The tool luc adds the fields it needs (kind, version, owner, license, tasks); the compiler reads only what is named here, but refuses a key set twice in one element, whoever reads it, at the line that sets it again. A file of the package outside its source root, a test program under tests/ say, imports the package's own modules by their paths under the source root, as a file inside it does. The package's modules come first and the modules beside the file next, never the file itself, so tests/parse.lucb may import parse and a helper under tests/ is imported by its own name.
16.5 Tests#
test "cursor advances by one":
var cursor = Cursor(data = "ab".bytes, offset = 0)
try cursor.advance(1)
assert(cursor.offset == 1)test is a declaration, compiled to a hidden unit! function and discovered statically; luce test runs every test and luce build removes them all. A test build runs the tests of the package under test: those of the file under test and of every module of its package that it imports, directly or not, module by module in the order the modules load (an import before its importer) and each module's in declaration order; with --package (what luc test asks for), every other module under the package's source root too, imported or not, after them in the order of their paths, each after what it imports, but not a hidden directory's or one holding a package of its own; a dependency's tests do not run. It compiles what its tests reach, of the file under test as of its imports, so a function no test calls is not compiled, and may name a symbol only another platform links. A test may use its module's private declarations. A package keeps test code out of the source it ships: under tests/, the directory that mirrors a module's path below the source root (tests/mime/parse/ for src/mime/parse.lucb or the fragment directory src/mime/parse/) holds test fragments that a TESTS file lists in order; a test build adds them to each tested module after the module's own source, in its scope, and a build never reads them. Inside a full Luce program tests run under the shared harness. In a Base artifact they run under a freestanding runner with a Base testing module providing assertions, deterministic seeds, and a fixed-buffer allocator made current for each test; facilities that need an isolated execution domain are absent. A test that fails, by an error or by a false assert written in its own body (not in a lambda or a deferred block in it, nor in a function it calls), is reported and the run goes on: the report is a line per test, ok name or FAIL name followed by an indented file:line:column: message, the failed assert's position and assert failed: condition (and its message), or the test's own position and the error's message; then N passed, and M failed with status 1 when any failed. Any other trap or fatal signal, an assert in a called function's included, ends the run: the test is reported FAIL name, the trap's own line follows on stderr, then the tally of the tests run so far, and the status is 1. A position names a source relative to the project root, the directory holding its package.prisma, in every build but a debug one; a file outside any package is named as it was given. A test that writes a module global is not isolated from the others; the runner is to report which globals it wrote (planned).
16.6 Standard modules#
The language depends on these modules by name. Their full surfaces are in the library reference; what the language needs is stated here. Their names are reserved: a package module named like one could never be imported, since the standard module's name always names it, so importing that name from a package that has such a module is an error. A standard module is used like any other: nothing is visible without an import, import io makes the module visible as io, and from io import Writer brings one of its declarations into scope by its bare name. Only the core types (str, ErrorCode, the scalars) and the core functions of §3.5 need no import.
| Module | What the language relies on |
|---|---|
memory | allocator (thread-local current allocator), heap (the initial allocator), exhausted and unset (error codes), allocate (§12.2), size_of, align_of, offset_of (§5.11), read, write, copy, move, set, grow, frame (§12.7), page_size() |
io | stdout() and stderr() as Writers; path.user(), path.home(), path.temp(), path.config() for the process's directories |
os | the target as constants: arm64, x86_64, macos, linux, windows, posix, pointer_bits, name, cpu_level (§19.5); cpu_level_running(), cpus(), page_size(), random_bytes, env, set_env, unset_env, cwd, change_dir, executable, pid, parent_pid, hostname, exit |
thread | spawn, Handle, current, pause, yield, sleep |
sync | Mutex, Condition, Once, Semaphore, Cancellation |
atomic | fence, Ordering |
luce | the protocols Equatable, Hashable, Comparable (§13.1), Iterator, Iterable and Display (§14.4); the source facts location, file, line, function (§9.1) |
strings | format (§5.5), copy, release, join, split, find, trim, replace, the number parsers and formatters, and write_i64 and its siblings, a value laid out by a specification to a Writer |
numerals | the format specification of a {value:spec} field (§5.5): read, misfit (and misfit_of, the reason as a Misfit for another wording), is_scalar, and lay_integer, lay_float, lay_text into a Sink; decimal_places, a float's exact decimal digits rounded half to even |
time | now, unix, since |
c | the C types of §5.2, errno(), set_errno(value), stdin(), stdout(), stderr() |
testing | assertions, seeds, and the per-test allocator of §16.5 |
These are the runtime: the modules the compiler and every program's startup need, shipped with the compiler and read from source with every build: src/std of a source tree, or share/luce-base/std beside a released compiler's bin directory (--std-dir DIR and the LUCE_STD environment variable name another place; ORDER there lists the modules in binding order, a module being one file or the fragments of §16.1). A standard module's name belongs to it: no program module may take one. The modules' globals initialise in that order, before the program's own.
Everything else is a package like any other, versioned apart from the compiler and imported through a def dependency of the program's manifest: luce-std holds files, paths, process, net, math, math32, utf8, unicode and crash; luce-window holds window and input; luce-gpu, luce-fonts, luce-clipboard and luce-dialogs hold one module each. The language gives them nothing a program's own packages lack: a package declares the native libraries its platform code needs in its own manifest (§17.4), and every imported module, standard or not, is compiled in one unit with the program for only the declarations the program reaches, so a program that never names window carries no window system and a program that never names unicode carries no Unicode tables. A library built with --lib carries what it reaches of its imports, so its C user links it alone.