# 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:

```loup
@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*

```text
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:

```loup,does_not_compile
@mode(host.native)

from core.math import sqrt, floor;

func main(): void {
    println(sqrt(2.0));
}
```

```text
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:

```text
inventory/
├── inventory.lppkg
└── src/
    ├── main.lphc
    ├── stock.lphc
    ├── format.lphc
    └── api.lphc
```

```toml
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:

```loup
@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:

```loup,ignore
@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:

```loup,ignore
@mode(host.native)

pub from .stock import (Item, lowStock);
pub from .format import (line as formatLine);
```

*Listing 13-5: `src/api.lphc`*

```loup,ignore
@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:

```sh
loup run src/main.lphc
loup-bootstrap check
loup build --target native -o inventory
```

```text
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 `pub`s from `Item`'s fields and `format.lphc` stops compiling:

```text
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:

```loup,ignore
@mode(host.native)

from stock import (Item, auditLog);

func main(): void {
    println(auditLog());
}
```

```text
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:

```text
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 `@mode` line, but imports still resolve by file path: a module in `src/stock.lphc` that declares `namespace inventory.stock;` is imported as `stock`, not `inventory.stock`. Keep namespaces equal to file paths, or leave them out.
- **Directory index modules.** `from report import (…)` resolving to `src/report/index.lphc` works under `loup run`, but `loup build` and `loup-bootstrap check` report `module 'report' not found` (`E-BUILD-DEP`, `E-CHECK-DEP`). Use a file such as `api.lphc` for 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 in `main()` 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*):

```toml
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 install` stops with "registry-based package installation is not yet available in this release", and there are no `loup pkg`, `loup add` or `loup publish` commands. A `{ path = … }` dependency is honoured by `loup build`, but not by `loup run` or `loup-bootstrap check`. For local packages, use `LOUPPATH`.

## `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>`:

```text
~/dev/loup-packages/            # a LOUPPATH root
└── textkit/
    ├── textkit.lppkg           # name = "textkit"
    └── slug.loup
```

```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*

```loup,ignore
@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*

```sh
$ 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);` and `import 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 under `loup run`.
- Import cycles are `E_CIRCULAR_IMPORT`.
- The manifest is TOML: `[build] entry`, `delivery`, `mode`, `profile`; `[modes]`; `[dependencies]`.
- No registry yet: `loup install` cannot fetch dependencies, and path dependencies work only with `loup build`. Use `LOUPPATH` roots for local packages.
- Chapter 14 covers template fences.
