Chapter 13: Modules and packages
A program larger than one file is a package: a directory with a .lppkg manifest and source files, each of which is a module. This chapter covers how modules import each other and the standard library, what pub exposes, re-exports, the manifest, and LOUPPATH, the way to use a package from another directory. It also says plainly what Loup 1.25.1 cannot do yet: there is no package registry, so loup install cannot fetch dependencies.
Importing from the standard library
An import names a module path and a parenthesised list of names. as renames a name, and import path as alias binds the whole module:
@mode(host.native)
from core.math import (sqrt, floor as fl);
import core.math as m;
func main(): void {
println(sqrt(2.0));
println(fl(2.7));
println(m.pow(2.0, 10.0));
}
Listing 13-1: Two forms of import
1.4142135623730951
2
1024
Imports are allowed only at module level, after the @mode line (and after a namespace line, if there is one). The prelude functions used throughout this book (println, readText, exec, env, …) need no import. docs/stdlib-status.md in the repository lists which standard-library modules ship in 1.25.1: among them core.math, core.time, core.random, io.fs, io.path, io.proc, system.json, system.text, system.regex, the data.* formats (csv, toml, yaml, xml, ini), net.tcp and the server half of net.http. Many specified modules, including sync.*, the native.* family and ui.*, do not exist yet.
Mistake: an import list without parentheses
Python writes from math import sqrt, floor. Loup requires the parentheses, even for one name:
@mode(host.native)
from core.math import sqrt, floor;
func main(): void {
println(sqrt(2.0));
}
main.lphc:3:23: error[E0001]: expected LParen, found Identifier
main.lphc:3:34: error[E0001]: expected RParen, found Semicolon
There is no wildcard import either: from core.math import *; adds error[E0007]: wildcard imports are not allowed. Name what you use, so that every name in a file can be traced to its source.
Modules in a package
Each source file is a module, named by its path relative to the source directory, without the extension. Here is a small stock-report program split into three modules and a manifest:
inventory/
├── inventory.lppkg
└── src/
├── main.lphc
├── stock.lphc
├── format.lphc
└── api.lphc
name = "inventory"
version = "0.1.0"
[build]
entry = "src/main.lphc"
delivery = "native"
Listing 13-2: inventory.lppkg
src/stock.lphc defines the data and the logic. Declarations without pub stay private to the module:
@mode(host.native)
pub struct Item {
pub sku: string;
pub count: int;
}
pub func lowStock(items: list[Item], threshold: int): list[Item] {
var result: list[Item] = [];
for (item in items) {
if (item.count < threshold) {
result.push(item);
}
}
return result;
}
func auditLog(): string {
return "audit";
}
Listing 13-3: src/stock.lphc
src/format.lphc imports from it by module name:
@mode(host.native)
from stock import (Item);
pub func line(item: Item): string {
return i"{$ item.sku $}: {$ item.count $} left";
}
Listing 13-4: src/format.lphc
src/api.lphc is a barrel module: it re-exports names from the others with pub from, so the rest of the program has one place to import from (Language Reference §1-13.2). The leading dot makes the path relative to the current module:
@mode(host.native)
pub from .stock import (Item, lowStock);
pub from .format import (line as formatLine);
Listing 13-5: src/api.lphc
@mode(host.native)
from api import (Item, lowStock, formatLine);
func main(): void {
let items = [
Item(sku = "bolt-m6", count = 4),
Item(sku = "nut-m6", count = 120),
Item(sku = "washer-m6", count = 9),
];
for (item in lowStock(items, 10)) {
println(formatLine(item));
}
}
Listing 13-6: src/main.lphc
(Listings 13-4 to 13-6 import other files of the package, so the book's single-file checker skips them; they were run as a project.) From the inventory directory:
loup run src/main.lphc
loup-bootstrap check
loup build --target native -o inventory
bolt-m6: 4 left
washer-m6: 9 left
loup run takes the entry file; check and build find the manifest in the current directory or the nearest parent, and build every module the entry reaches.
Mistake: forgetting pub on struct fields
Struct fields are private to their module unless marked pub (Language Reference §1-10.2.1). Inside stock.lphc that never shows. Remove the two pubs from Item's fields and format.lphc stops compiling:
src/format.lphc:6:17: error[E_FIELD_PRIVATE]: member 'sku' of struct 'Item' is private
src/format.lphc:6:33: error[E_FIELD_PRIVATE]: member 'count' of struct 'Item' is private
The constructor still accepts every field by name; privacy only governs reading and writing item.sku from another module. Class members follow the same rule (chapter 9).
Mistake: importing a private function
auditLog has no pub, so another module cannot import it:
@mode(host.native)
from stock import (Item, auditLog);
func main(): void {
println(auditLog());
}
src/main.lphc:3:26: error[E0001]: cannot import private symbol `auditLog` from module
note: add `pub` to the declaration to make it importable
src/main.lphc:6:13: error[E0001]: undefined identifier `auditLog`
Cycles
Two modules that import each other are rejected:
src/main.lphc:3:1: error[E_CIRCULAR_IMPORT]: import cycle detected: …/src/a.lphc → …/src/b.lphc → …/src/a.lphc
note: circular imports are not allowed; consider restructuring your modules
Move what both need into a third module that imports neither. The specification also offers from x defer import (T), which imports a type as a stub to break a cycle (Language Reference §1-13.10); reach for it only when the cycle cannot be designed away.
Things that do not work as specified
namespace a.b;is accepted after the@modeline, but imports still resolve by file path: a module insrc/stock.lphcthat declaresnamespace inventory.stock;is imported asstock, notinventory.stock. Keep namespaces equal to file paths, or leave them out.- Directory index modules.
from report import (…)resolving tosrc/report/index.lphcworks underloup run, butloup buildandloup-bootstrap checkreportmodule 'report' not found(E-BUILD-DEP,E-CHECK-DEP). Use a file such asapi.lphcfor a barrel, as above. - Top-level initializers. The Language Reference allows only constant initializers at the top of an AOT-mode module. Stage0 accepts
let started = runtime.args();there without complaint. Compute runtime values inmain()and pass them on.
The manifest
The manifest is a TOML file named <package-name>.lppkg (or package.lppkg). These are the keys a native project uses (Language Reference §1-13.3; Loup Packages Reference):
name = "inventory"
version = "0.1.0"
# minimum toolchain version
loup = "1.25.0"
[build]
# the module whose main() starts the program
entry = "src/main.lphc"
# what to build: native, cli (WebAssembly), desktop, ...
delivery = "native"
# mode for files that declare none (every file should declare one)
mode = "host.native"
# or "debug"
profile = "release"
[modes]
# per-file mode for files without a directive
"src/legacy.loup" = "host.dual"
[dependencies]
textkit = { path = "../textkit" }
Listing 13-7: The parts of a manifest a native project uses
Put comments on lines of their own. The 1.25.1 manifest reader does not accept a comment after a value on the same line: loup = "1.25.0" # minimum is reported as invalid patch version: '0" # minimum'.
delivery was called target before 1.21; both spellings are accepted, as are [delivery.X] and [target.X] tables. loup-bootstrap init writes a manifest with these keys and several empty tables (chapter 1).
Dependencies
[dependencies] takes a version constraint (textkit = "^0.2") or a local path ({ path = "…" }).
⚠ Planned. The package registry is not available in Loup 1.25.1. As soon as a manifest declares a dependency,
loup installstops with "registry-based package installation is not yet available in this release", and there are noloup pkg,loup addorloup publishcommands. A{ path = … }dependency is honoured byloup build, but not byloup runorloup-bootstrap check. For local packages, useLOUPPATH.
LOUPPATH
LOUPPATH is a list of directories, separated by : (; on Windows), each of which contains packages (Language Reference §1-13.9, ruling PATH-1). A package named textkit lives in <root>/textkit/ with its own textkit.lppkg, and its modules are imported as textkit.<module>:
~/dev/loup-packages/ # a LOUPPATH root
└── textkit/
├── textkit.lppkg # name = "textkit"
└── slug.loup
@mode(host.dual)
pub func slugify(title: string): string {
return title.trim().toLowerCase().replace(" ", "-");
}
Listing 13-8: textkit/slug.loup, a host.dual library module
@mode(host.native)
from textkit.slug import (slugify);
func main(): void {
println(slugify(" Hello Native World "));
}
Listing 13-9: Using the package from a native program
$ loup run src/main.lphc
src/main.lphc:3:6: error[E0001]: unresolved package import 'textkit/slug': searched package roots ['src'] for the package path 'textkit/slug'
$ LOUPPATH=~/dev/loup-packages loup run src/main.lphc
hello-native-world
$ LOUPPATH=~/dev/loup-packages loup build --target native -o blog
Without LOUPPATH the import is unresolved; with it, loup run and loup build find the package. (loup-bootstrap check ignores LOUPPATH in 1.25.1 and reports E-CHECK-DEP.) Writing library code as host.dual, as textkit does, lets scripts, WebAssembly programs and native programs all use it; a host.native program may import host.dual modules freely.
The standard library's namespaces (core, io, data, net, sync, native, ui, web, lang, debug, structures, concurrent, and the loup. prefix) are reserved: a package on LOUPPATH may not supply modules under them (E_IMPORT_RESERVED_NAMESPACE), though your own program's source tree may.
Summary
from path import (a, b as c);andimport path as m;; the list always has parentheses, and there are no wildcards.- Each file is a module named by its path under the source directory; declarations and struct fields are private unless
pub(cannot import private symbol,E_FIELD_PRIVATE). pub from .module import (…)re-exports; keep barrels in ordinary files, because directory index modules work only underloup run.- Import cycles are
E_CIRCULAR_IMPORT. - The manifest is TOML:
[build] entry,delivery,mode,profile;[modes];[dependencies]. - No registry yet:
loup installcannot fetch dependencies, and path dependencies work only withloup build. UseLOUPPATHroots for local packages. - Chapter 14 covers template fences.