# Chapter 13: Modules and Packages

**What you'll learn.** How files become modules, the `from … import (…)` and `import … as …` forms, `pub` visibility across files, barrel modules with `pub from`, the import rule between `web.compiled` and everything else, circular imports and `defer import`, the `.lppkg` manifest that `loup-bootstrap init` writes, and what the package tooling can and cannot do in Loup 1.25.

**Why it matters.** Up to now every program has been one file. Real projects are split across files and directories, and in the web modes the import rule decides which code can reach the standard library at all.

## A project with more than one file

Start with the scaffold from Chapter 1 and give it a second directory. Here is a small shop project with a `cart` package of two modules:

```text
shop/
  shop.lppkg
  src/
    main.lpweb
    cart/
      money.lpweb
      discount.lpweb
  tests/
```

Every source file is a module. Its import path is its location under `src/`, with directories joined by dots and the extension dropped: `src/cart/money.lpweb` is the module `cart.money`. Here is that module:

```loup
@mode(web.compiled)

pub struct Money {
    pub cents: int;
}

pub func format(m: Money): string {
    let rest = m.cents % 100;
    let dollars = (m.cents - rest) / 100;
    let pad = if (rest < 10) { "0" } else { "" };
    return "$" + str(dollars) + "." + pad + str(rest);
}

func checkCents(cents: int): void {
    if (cents < 0) {
        throw ValueError("negative amount");
    }
}

pub func dollars(d: int, c: int): Money {
    checkCents(c);
    return Money(cents = d * 100 + c);
}
#~
#~ func main(): void {}
```

*Listing 13-1: `src/cart/money.lpweb`, a module with three public names and one private helper*

The module is declarations only, as every `web.compiled` file must be (Chapter 2). `pub` marks what other modules may import: the struct `Money`, its field `cents`, and the functions `format` and `dollars`. `checkCents` has no `pub`, so it is private to this file. Struct and class fields are private to their module unless they are `pub` too; without `pub cents`, code in another module could build a `Money` but not read `price.cents` (`error[E_FIELD_PRIVATE]: member 'cents' of struct 'Money' is private`).

The second module uses a *relative* import. A leading dot means "next to this file":

```loup,ignore
@mode(web.compiled)

from .money import (Money);

pub func applyPercent(m: Money, percent: int): Money {
    let off = m.cents * percent / 100;
    return Money(cents = m.cents - off);
}
```

*Listing 13-2: `src/cart/discount.lpweb` imports a sibling module with a relative path*

The entry file imports from both:

```loup,ignore
@mode(web.compiled)

from cart.money import (Money, format, dollars);
from cart.discount import (applyPercent);

func main(): void {
    let price = dollars(19, 99);
    let sale = applyPercent(price, 15);
    println(format(price) + " -> " + format(sale));
}
```

*Listing 13-3: `src/main.lpweb` imports from two modules of the project*

Listings 13-2 and 13-3 are marked `ignore` only because the book's listing checker compiles one file at a time; the project as a whole was run with the 1.25.1 toolchain:

```sh
$ loup run src/main.lpweb
$19.99 -> $17.00
```

A `web.compiled` module can import another `web.compiled` module, both under `loup run` and in a Wasm build. `loup-bootstrap build --target browser` compiles all three modules into one `build/shop.wasm`, which `loup-launcher` runs with the same output:

```sh
$ loup-bootstrap build --target browser
Resolving dependencies...
Compiling 3 module(s)...
Generating code...
Build complete: …/shop/build/shop.wasm (21953 bytes)
…/shop/build/shop.wasm
$ loup-launcher --run-bare build/shop.wasm
$19.99 -> $17.00
```

### Importing a private name

If `main.lpweb` reaches for the private helper:

```loup,ignore
@mode(web.compiled)

from cart.money import (dollars, checkCents);

func main(): void {
    checkCents(-1);
    println(dollars(1, 0).cents);
}
```

the compiler refuses the import and every use of the name:

```text
src/main.lpweb:3:34: error[E0001]: cannot import private symbol `checkCents` from module
  note: add `pub` to the declaration to make it importable
src/main.lpweb:6:5: error[E0001]: undefined identifier `checkCents`
```

The fix is the one the note gives — add `pub` to `checkCents` — or, better here, leave it private: `dollars` already calls it, and callers outside the module have no reason to.

Loup has two levels of visibility: `pub` (importable) and private (the declaring module only). There is no `pub(package)` or `pub(module)`; writing one is a parse error.

## The import forms

All imports go at the top of the file, after `@mode(...)` and an optional `namespace` line. They cannot appear inside a function. Wildcard imports (`import *`) do not exist: every imported name is written out.

| Form | Binds |
|---|---|
| `from cart.money import (Money, format);` | the names `Money` and `format` |
| `from cart.money import (format as formatMoney);` | `formatMoney` only |
| `from .money import (Money);` | a name from a sibling module |
| `from cart { money import (Money); discount import (applyPercent); }` | grouped imports under one prefix |
| `import cart.money as money;` | the module, used as `money.format(…)` |
| `import cart.money;` | the module under its full path, `cart.money.format(…)` |

The import list always has parentheses, even for one name. The next listing is a `web.scripted` file, which can import the standard library (more on that below), and it leaves the parentheses out:

```loup,does_not_compile
@mode(web.scripted)

from core.math import sqrt, floor;

println(sqrt(16.0));
println(floor(2.7));
```

```text
main.lpweb:3:23: error[E0001]: expected LParen, found Identifier
main.lpweb:3:34: error[E0001]: expected RParen, found Semicolon
```

"Expected LParen" points at `sqrt`: the parser wanted `(` after `import`. Add the parentheses:

```loup
@mode(web.scripted)

from core.math import (sqrt, floor);

println(sqrt(16.0));
println(floor(2.7));
```

*Listing 13-4: An import list is always parenthesized*

```text
4
2
```

### An alias replaces the name

`as` binds the imported symbol under the new name *instead of* the original. A reader who renames `sqrt` and then keeps using `sqrt` gets:

```loup,does_not_compile
@mode(web.scripted)

from core.math import (sqrt as root);

println(sqrt(16.0));
```

```text
main.lpweb:5:9: error[E0001]: undefined identifier `sqrt`
```

Use the alias, or import the name twice if you want both spellings (`from core.math import (sqrt as root, sqrt);`):

```loup
@mode(web.scripted)

from core.math import (sqrt as root, floor);

println(root(16.0));
println(floor(2.7));
```

```text
4
2
```

### Importing a module

`import path as name;` binds the whole module. Its public names are reached through the alias, which keeps it obvious at each call site where a name comes from:

```loup
@mode(web.scripted)

import core.math as math;

let r = 3.0;
println(math.PI * r * r);
println(math.sqrt(2.0));
```

*Listing 13-5: A module import, with members reached as `math.PI` and `math.sqrt`*

```text
28.274333882308138
1.4142135623730951
```

A module alias names a scope, not a value: `math` on its own is an error, and only the module's `pub` names are members of it.

## Barrel modules with `pub from`

A directory can have an `index.lpweb` that re-exports what the rest of the program should use. Importing the directory's path (`from cart import …`) finds that index:

```loup,ignore
@mode(web.compiled)

pub from .money import (Money, format, dollars);
pub from .discount import (applyPercent);
```

*Listing 13-6: `src/cart/index.lpweb`, a barrel module*

Callers no longer need to know which file declares what (this listing and the barrel are run as part of the project, like Listing 13-3):

```loup,ignore
@mode(web.compiled)

from cart import (dollars, format, applyPercent);

func main(): void {
    let sale = applyPercent(dollars(19, 99), 15);
    println(format(sale));
}
```

```text
$17.00
```

A re-export can rename (`pub from .money import (format as formatMoney);`), and it can only re-export `pub` names. Re-exporting `checkCents` from the barrel fails in the barrel itself with `error[E0001]: cannot re-export private symbol `checkCents` from module …` and the note `only `pub` symbols can be re-exported`.

## `namespace` declarations

A file may declare a namespace right after its `@mode` line:

```loup
@mode(web.compiled)

namespace shop.cart;

pub func lineTotal(unitCents: int, qty: int): int {
    return unitCents * qty;
}

func main(): void {
    println(lineTotal(450, 3));
}
```

```text
1350
```

The Language Reference (§1-13.0) says a file's public names are exposed under its namespace path. In Loup 1.25 the resolver still finds modules **by file path only**: with that line in `src/cart/pricing.lpweb`, `from cart.pricing import (…)` works and `from shop.cart import (…)` fails with `unresolved package import`. Until that changes, either leave `namespace` out or make it match the file's path.

## The import rule for `web.compiled`

A `web.compiled` module may import **only other `web.compiled` modules** (Language Reference §3-1.2). Every module of the standard library is a `host.scripted` module, so in Loup 1.25 a `web.compiled` file cannot import any of them. The prelude — `println`, `str`, `repr`, `Ok`, `Some`, the exception classes and the other names Chapter 2 lists — needs no import and is available everywhere.

This is the mistake you will make first:

```loup,does_not_compile
@mode(web.compiled)

from core.math import (sqrt);

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

```text
main.lpweb:3:1: error[E0001]: `@mode(web.compiled)` module cannot import `@mode(host.scripted)` module
  note: `@mode(web.compiled)` modules can only import other `@mode(web.compiled)` modules
  note: error code: E_COMPILED_IMPORT_SCRIPTED
```

The code is `E_COMPILED_IMPORT_SCRIPTED`, the third line says so. There are two ways out. If the program needs the library module, write it in `web.scripted`, which may import the standard library and runs its top-level statements in order (no `main`):

```loup
@mode(web.scripted)

from core.math import (sqrt);

println(sqrt(2.0));
```

*Listing 13-7: The same program in `web.scripted`, which can import `core.math`*

```text
1.4142135623730951
```

If the program must stay `web.compiled`, keep library calls out of it, or write the small piece you need yourself in a `web.compiled` module of your own.

The same rule applies to your own files. If `src/cart/tax.lpweb` is a `web.scripted` module, importing it from `main.lpweb` (a `web.compiled` module) fails with the same code:

```text
src/main.lpweb:3:1: error[E0001]: `@mode(web.compiled)` module cannot import `@mode(web.scripted)` module
  note: `@mode(web.compiled)` modules can only import other `@mode(web.compiled)` modules
  note: error code: E_COMPILED_IMPORT_SCRIPTED
```

The other direction is allowed. A `web.scripted` file can import your `web.compiled` modules *and* the standard library, which makes it the natural home for a tool that glues the two together. This one reads a price list with `data.csv` and prints it with the `cart` package:

```loup,ignore
@mode(web.scripted)

from data.csv import (parse);
from cart import (dollars, format, applyPercent);

let rows = parse("tea,4,50\nmug,12,0", ",");
for (row in rows) {
    let price = dollars(row[1].to_int(), row[2].to_int());
    let sale = applyPercent(price, 10);
    println(row[0] + ": " + format(price) + " (sale " + format(sale) + ")");
}
```

*Listing 13-8: `src/pricelist.lpweb`, a `web.scripted` tool that imports both the project's `web.compiled` modules and the standard library*

```sh
$ loup run src/pricelist.lpweb
tea: $4.50 (sale $4.05)
mug: $12.00 (sale $10.80)
```

```text
   web.scripted ───────────► web.compiled     allowed
   web.scripted ───────────► standard library allowed
   web.compiled ───────────► web.compiled     allowed
   web.compiled ─ ─ ─ ─ ─ ─► web.scripted     E_COMPILED_IMPORT_SCRIPTED
   web.compiled ─ ─ ─ ─ ─ ─► standard library E_COMPILED_IMPORT_SCRIPTED (1.25)
```

> ⚠ **Planned.** The Language Reference describes the browser modules (`web.dom`, `web.storage`, `web.workers`, `ui.web`, `lang.interop.js`, …) and says web code gets the full standard library apart from host capabilities. None of the `web.*` or `ui.*` modules ships in Loup 1.25 — chapters 16–18 describe them as design previews.

### Reserved namespaces

The roots `core`, `io`, `data`, `net`, `sync`, `concurrent`, `web`, `ui`, `native`, `lang`, `debug` and `structures`, and the `loup.` prefix, are reserved for the standard library and first-party packages (Language Reference §1-13.6). An import under a reserved root must name a module the standard library actually has (or one your own `src/` tree supplies). Importing a planned browser module therefore fails even though the name is "right":

```loup,does_not_compile
@mode(web.compiled)

from web.dom import (query);

func main(): void {
    println("ready");
}
```

```text
main.lpweb:3:6: error[E0001]: 'web.dom' is not a standard-library module (namespace 'web' is reserved for the standard library)
```

There is no fix in 1.25 other than not using the module; the list of modules that ship is in the standard library status page of the Loup repository (`docs/stdlib-status.md`).

## Circular imports and `defer import`

Two modules that import each other form a cycle, and the compiler rejects it in every mode (`E_CIRCULAR_IMPORT`, Language Reference §1-13.10). An employee knows its team and a team lists its employees:

```loup,ignore
@mode(web.compiled)

from org.team import (Team);

pub class Employee {
    pub let name: string;
    pub var team: Option[Team] = None;

    new(name: string) {
        this.name = name;
    }
}
```

*Listing 13-9: `src/org/employee.lpweb` imports `Team`*

```loup,ignore
@mode(web.compiled)

from org.employee import (Employee);

pub class Team {
    pub let title: string;
    pub var members: list[Employee] = [];

    new(title: string) {
        this.title = title;
    }
}
```

*Listing 13-10: `src/org/team.lpweb` imports `Employee`*

A program that imports either module stops at the cycle (paths shortened from the absolute paths the compiler prints):

```text
src/staff.lpweb:3:1: error[E_CIRCULAR_IMPORT]: import cycle detected: src/org/team.lpweb → src/org/employee.lpweb → src/org/team.lpweb
  note: circular imports are not allowed; consider restructuring your modules
```

The usual fixes are to move the shared types into a third module that both import, or to merge the two modules. When the two types really do refer to each other, one side can import the other's name as a *deferred type stub*: change the first line of `employee.lpweb` to

```loup,ignore
from org.team defer import (Team);
```

`defer import` gives `Team` a type identity at once and binds its definition after module initialization, which breaks the cycle. With that one change this program runs:

```loup,ignore
@mode(web.compiled)

from org.team import (Team);
from org.employee import (Employee);

func main(): void {
    let web = Team("Web");
    let ann = Employee("Ann");
    safe {
        ann.team = Some(web);
        web.members.push(ann);
    }
    println(web.title + " has " + str(web.members.len()) + " member(s)");
    match ann.team {
        Some(t) => println(ann.name + " is on " + t.title),
        None => println(ann.name + " has no team"),
    }
}
```

*Listing 13-11: `src/staff.lpweb` uses both classes once the cycle is broken with `defer import`*

```text
Web has 1 member(s)
Ann is on Web
```

(The `safe { … }` block is there because writing a `var` field of a class instance needs a safe context; Chapter 9 explains why.) Listings 13-9 to 13-11 are marked `ignore` because they span several files; they were run together as a project with the 1.25.1 toolchain. A deferred cycle is still a hint that the modules might be split differently.

## Module dunders

Every module can read three string constants: `__name__`, `__file__` and `__execmode__`. They are handy in log lines:

```loup
@mode(web.compiled)

func whereAmI(): string {
    return __name__ + " in " + __file__ + " (" + __execmode__ + ")";
}

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

```text
__main__ in main.lpweb (interpreted)
```

Do not branch on these values yet. The Language Reference says that in the web modes `__name__` is the module's dotted name and never `"__main__"`, and that `__execmode__` is the mode string (`"web.compiled"`). Under `loup run` in 1.25 `__name__` is `"__main__"`, `__execmode__` is `"interpreted"`, and all three report the *entry* file's values even inside an imported module: moved into `src/cart/locate.lpweb` and called from `src/d.lpweb`, `whereAmI()` prints `__main__ in src/d.lpweb (interpreted)`.

## The package manifest

A project is a directory with a `.lppkg` manifest. `loup-bootstrap init` writes one (Chapter 1 explains why this book uses `loup-bootstrap` for `init`, `check` and `build`):

```sh
$ loup-bootstrap init shop
created project at …/shop
```

It creates `shop/shop.lppkg`, `shop/src/main.loup` and an empty `shop/tests/`. The scaffold is a `host.dual` program; for a web project, rename `src/main.loup` to `src/main.lpweb`, change its first line to `@mode(web.compiled)`, and point `entry` at it. The top of the manifest then reads:

```toml
name = "shop"
version = "0.1.0"
loup = "1.20.0"
authors = []

[build]
entry = "src/main.lpweb"
delivery = "cli"
profile = "release"
strict_deps = false
separate = false
```

`loup-bootstrap init` also writes empty `[test]`, `[dependencies]`, `[dev_dependencies]`, `[delivery]`, `[stdlib]`, `[modes]`, `[linker]`, `[freestanding]`, `[options]`, `[const_eval]` and `[exports]` tables, which you can leave alone. The keys you will touch:

- `name`, `version` — the package's name (lowercase, hyphen-separated segments) and its semantic version. `loup` is the minimum toolchain version.
- `[build] entry` — the file `loup-bootstrap build` starts from.
- `[build] delivery` — the artifact shape: `cli` by default, `browser` or `wasm` for a web module. `loup-bootstrap build --delivery browser` and its older spelling `loup-bootstrap build --target browser` override it; manifests may likewise still say `target` for `delivery` and `[target.X]` for `[delivery.X]`. Today every one of these shapes produces a bare `build/<name>.wasm` (Chapter 1).
- `[modes]` and `[build] mode` — project defaults for files that declare no mode. Ruling MODE-2 says every file should declare its own mode, so you should not need them.

The full schema is in the Language Reference §1-13.3.

## Dependencies and `LOUPPATH`

> ⚠ **Planned.** The package registry is not available in Loup 1.25. `loup add`, `loup pkg`, `loup publish` and `loup link` do not exist (`error: unknown command 'add'`), `loup.lock` is never written, and `[dev-overrides]` and the Core Package Set (`loup.http`, `loup.db`, …) are described in the Language Reference and the Packages Reference but not usable.

Declaring a registry dependency in `[dependencies]` makes `loup install` stop with:

```text
error: 1 external dependency declared, but registry-based package installation is not yet available in this release
  note: the package registry and the `loup install`/`add`/`publish` network paths are planned; see the Packages Reference
```

What does work is `LOUPPATH`. It is a colon-separated list (semicolons on Windows) of *roots*: directories that contain packages. A package named `acme.units` lives at `<root>/acme/units/` beside its manifest `acme.units.lppkg`:

```text
/home/you/vendor/            # a root on LOUPPATH
  acme/                      # namespace directory, no manifest
    units/
      acme.units.lppkg       # name = "acme.units"
      length.lpweb           # module acme.units.length
```

```loup
@mode(web.compiled)

pub func cmToInches(cm: float): float {
    return cm / 2.54;
}
#~
#~ func main(): void {}
```

*Listing 13-12: `length.lpweb` in the `acme.units` package*

With the root on `LOUPPATH`, `from acme.units.length import (cmToInches);` resolves from the shop project:

```sh
$ LOUPPATH=/home/you/vendor loup run src/main.lpweb
10
```

Without it, the same run fails with `unresolved package import 'acme/units/length': searched package roots ['src'] …`. In 1.25 `loup run` and `loup-bootstrap build` honour `LOUPPATH`; `loup-bootstrap check` does not yet (it reports `E-CHECK-DEP`). A path dependency in `[dependencies]` (`"acme.units" = { path = "../vendor/acme/units" }`) is the reverse: `loup-bootstrap build` uses it and `loup run` does not. Prefer `LOUPPATH` for now.

## `prefer compiled`

The Language Reference describes *module fallback pairs* (§1-13.7): a package ships a compiled artifact and a scripted source for the same module, and an import written `from codec import (Encoder) prefer compiled;` loads the compiled one when it can. In `web.compiled` the clause is meant to be redundant (the spec gives it a `prefer-compiled-redundant` lint).

> ⚠ **Planned.** Loup 1.25 parses `prefer compiled` and ignores it — no lint, no fallback search, no `.wasm` artifact lookup. Write plain imports.

## A project layout

Putting the chapter together:

```text
shop/
  shop.lppkg               # manifest: [build] entry = "src/main.lpweb"
  src/
    main.lpweb             # web.compiled entry, func main()
    pricelist.lpweb        # web.scripted tool: imports cart + data.csv
    staff.lpweb            # web.compiled
    cart/
      index.lpweb          # barrel: pub from .money / .discount
      money.lpweb
      discount.lpweb
    org/
      employee.lpweb       # from org.team defer import (Team)
      team.lpweb
  tests/
  build/                   # loup-bootstrap build output: shop.wasm
```

`src/` holds the program's modules; everything in it is found by path. Files that need the standard library are `web.scripted`; everything that will be compiled to the Wasm module is `web.compiled` and imports only other `web.compiled` files.

## Summary

- Every source file is a module whose import path is its location under `src/`. Imports are module-level, list every name in parentheses, and come in `from … import (…)`, relative (`from .money`), grouped, and `import … as m` forms. An alias replaces the original name.
- `pub` makes a declaration, or a struct/class field, importable; everything else is private to its file. There is no `pub(package)`.
- `pub from .x import (…)` builds barrel modules; only `pub` names can be re-exported.
- A `web.compiled` module imports only `web.compiled` modules (`E_COMPILED_IMPORT_SCRIPTED`). In 1.25 that excludes the whole standard library; use `web.scripted` for code that needs it. `web.scripted` may import both.
- `web.*` and `ui.*` modules are planned; importing one is an error today.
- Import cycles are `E_CIRCULAR_IMPORT`; `defer import` breaks a cycle between types.
- `loup-bootstrap init` writes the `.lppkg` manifest; `[build] entry` and `delivery` are the keys a web project changes. The registry, `loup add`, `loup.lock` and `prefer compiled` resolution are planned; `LOUPPATH` works with `loup run` and `loup-bootstrap build`.
