# Chapter 11: Modules and Packages

**What you'll learn:** how one script uses code from another file, how `pub` decides what a file exports, the import forms Loup has (and the ones it deliberately lacks), what happens when a module is imported, how a project is laid out, and what the 1.25 toolchain offers for sharing packages.

## Every File Is a Module

Any Loup source file can be imported by another. The import path is the file's path relative to the importing script's directory, with `/` written as `.` and the extension left off. With this layout:

```text
wordtools/
├── main.lpscr
└── textutil/
    ├── slug.lpscr
    └── stats.lpscr
```

`main.lpscr` imports `textutil/slug.lpscr` as `textutil.slug`. Inside a project (later in this chapter), paths are relative to the project's `src/` directory instead.

## Exporting with `pub`

Declarations are private to their file unless they are marked `pub`:

```loup
@mode(host.scripted)

## Lower-case `text` and join its words with hyphens.
pub func slugify(text: string): string {
    let words = text.trim().toLowerCase().split(" ");
    let kept = words.filter(func(w: string) => w.length() > 0);
    return kept.join("-");
}

## Not exported: only this file can call it.
func isSeparator(c: string): bool {
    return c == " " or c == "-";
}
```

*Listing 11-1: textutil/slug.lpscr exports one function*

`pub` works on functions, classes, structs, enums, interfaces and module-level `let` bindings. There is no `export` keyword and no `private`: a name is either `pub` or file-private.

```loup
@mode(host.scripted)

## Number of space-separated words in `text`.
pub func wordCount(text: string): int {
    return text.split(" ").filter(func(w: string) => w.length() > 0).length();
}
```

*Listing 11-2: textutil/stats.lpscr*

## Importing Names

`from module import (names);` binds the listed names in the importing file. `as` renames one:

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

from textutil.slug import (slugify);
from textutil.stats import (wordCount as countWords);

let title = "  Error Handling in Loup  ";
println(slugify(title));
println(countWords(title));
```

*Listing 11-3: main.lpscr imports from two sibling modules (a multi-file example)*

```sh
loup run main.lpscr
```

```text
error-handling-in-loup
4
```

The alias replaces the original name: after `wordCount as countWords`, only `countWords` is bound.

Two other forms are useful. `import module as name;` binds the whole module, and you reach its exports through the name. A grouped import shares a prefix:

```loup
@mode(host.scripted)

import core.math as m;
from core {
    math import (sqrt, floor as roundDown);
}

println(m.sqrt(2.0));
println(sqrt(16.0));
println(roundDown(2.7));
```

*Listing 11-4: A module import and a grouped import*

```text
1.4142135623730951
4
2
```

(`sqrt(16.0)` is the `float` 4.0; Loup prints a whole-number `float` without the `.0`.)

Imports may appear only at the top level of a file, never inside a function or block.

### Mistake: an import list without parentheses

Python writes `from math import sqrt, floor`. Loup requires the parentheses:

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

from core.math import sqrt;

println(sqrt(2.0));
```

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

Write `from core.math import (sqrt);` — parentheses even around a single name.

### Mistake: a wildcard import

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

from core.math import *;

println(sqrt(2.0));
```

```text
main.lpscr:3:23: error[E0001]: expected LParen, found Star
main.lpscr:3:23: error[E0007]: wildcard imports are not allowed
main.lpscr:3:24: error[E0001]: expected RParen, found Semicolon
```

Every imported name is listed, so a reader can always see where a name came from. If a list grows long, import the module instead: `import core.math as m;`.

### Mistake: importing a private name

If `main.lpscr` asks `textutil/slug.lpscr` for its private helper:

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

from textutil.slug import (isSeparator);

println(isSeparator(" "));
```

```text
main.lpscr:3:28: error[E0001]: cannot import private symbol `isSeparator` from module
  note: add `pub` to the declaration to make it importable
main.lpscr:5:9: error[E0001]: undefined identifier `isSeparator`
```

The note says what to do if the helper should be public. Often the better fix is to leave it private and use the public function that wraps it.

## Re-Exports and Directory Modules

A directory can have an `index` module that gathers its contents into one import. `pub from … import (…)` re-exports names, optionally renamed:

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

pub from .slug import (slugify);
pub from .stats import (wordCount as countWords);
```

*Listing 11-5: textutil/index.lpscr re-exports from its sibling files*

Importing the directory itself now finds the index:

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

from textutil import (slugify, countWords);

println(slugify("Modules and Packages"));
println(countWords("Modules and Packages"));
```

```text
modules-and-packages
3
```

The leading dot in `from .slug` makes the path relative to the file that contains the import. Relative imports keep a directory of modules movable: rename `textutil/` and its index still works.

Only `pub` names can be re-exported (`E_REEXPORT_NOT_PUB`, §1-13.2). Rust's `pub use` and JavaScript's `export … from` do not exist in Loup; `pub from` is the one re-export form.

## What Runs When a Module Is Imported

```loup
@mode(host.scripted)

println("loading settings");

pub let defaultPort: int = 8080;
pub let defaultHost: string = "localhost";
pub let baseUrl: string = i"http://{$ defaultHost $}:{$ defaultPort $}";

if (__name__ == "__main__") {
    println(i"settings self-check: {$ baseUrl $}");
}
```

*Listing 11-6: settings.lpscr, a module with a self-check*

Run directly, every top-level statement runs:

```text
loading settings
settings self-check: http://localhost:8080
```

Imported by another script:

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

from settings import (baseUrl, defaultPort);

println(i"serving {$ baseUrl $} (port {$ defaultPort $})");
```

```text
serving http://localhost:8080 (port 8080)
```

The imported module's `let` initializers ran — `baseUrl` has its value — but `loading settings` was not printed.

> **Module initialization today.** The Language Reference (§1-13.4) says that importing a module runs all of its top-level statements once, in order, with `__name__` set to the module's dotted name. In Loup 1.25, `loup run` compiles the entry script and everything it imports as a single program: an imported module's declarations and `let`/`var` initializers take effect, but its other top-level statements do not run, and `__name__` is `"__main__"` in every module. The main guard still behaves as intended (the guarded block runs only in the file you run), but do not rely on an imported module's top-level statements for setup: put setup in a function and call it.

Give exported `let` bindings an explicit type, as Listing 11-6 does. Without one, an importer may be told `type of 'defaultPort' is not yet known`.

## Namespaces

A file may declare the dotted path it represents, right after its `@mode` line:

```loup
@mode(host.scripted)
namespace textutil.slug;

pub func slugify(text: string): string {
    let words = text.trim().toLowerCase().split(" ");
    let kept = words.filter(func(w: string) => w.length() > 0);
    return kept.join("-");
}
```

The declaration should match the file's location (§1-13.0); tools treat a mismatch as a lint warning. In the 1.25 toolchain, `loup run` still finds a module by its file path, so the declaration documents the path rather than changing it. Most scripts leave it out.

## Circular Imports

Two modules that import each other are an error:

```text
a.lpscr:2:1: error[E_CIRCULAR_IMPORT]: import cycle detected: …/a.lpscr → …/b.lpscr → …/a.lpscr
  note: circular imports are not allowed; consider restructuring your modules
```

(The message prints the full paths; they are shortened here.) Usually the fix is to move what both need into a third module. When two types genuinely refer to each other, one side can use `defer import`, which gives the name a type immediately and binds its definition later (§1-13.10):

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

from edges defer import (Edge);

pub class Node {
    pub let name: string;
    pub var edges: list[Edge] = [];

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

*Listing 11-7: nodes.lpscr breaks the cycle with `defer import`*

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

from nodes import (Node);

pub class Edge {
    pub let src: Node;
    pub let dst: Node;

    new(src: Node, dst: Node) {
        this.src = src;
        this.dst = dst;
    }
}
```

*Listing 11-8: edges.lpscr imports `Node` normally*

A script that imports both can build a graph: `Edge(Node("a"), Node("b"))`.

## `prefer compiled`

> ⚠ **Planned.** `from codec import (Encoder) prefer compiled;` asks the runtime to load a compiled build of the module when one exists and fall back to the source otherwise; the declaration's `__module__.__execmode__` would then report which happened (§1-13.7). The 1.25 toolchain accepts the clause but always uses the source, and `__module__` is not implemented.

## The Standard Library

The standard library is imported like any other module. These namespaces ship in 1.25:

| Namespace | Modules |
|---|---|
| `system` | `json`, `text`, `regex`, `collections`, `env`, `time` (the prelude — `println`, `readText`, `args`, … — is available without an import) |
| `core` | `math`, `time`, `datetime`, `random`, `uuid`, `string_utils`, `shlex`, `waiting`, `complex`, `array`, … |
| `io` | `fs`, `path`, `proc`, `streams` |
| `data` | `csv`, `ini`, `toml`, `xml`, `yaml`, `query` |
| `net` | `tcp`, `addressing`, `http` (a server; there is no HTTP client yet) |
| `structures` | `case_insensitive_dict`, `ordered_set`, `priority_queue` |
| `debug` | `assert`, `profiler` |

Some module names you may expect do not exist. JSON lives in `system.json`, not `data.json`:

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

from data.json import (parse);

println(parse("[1, 2]"));
```

```text
main.lpscr:3:6: error[E0001]: 'data.json' is not a standard-library module (namespace 'data' is reserved for the standard library)
main.lpscr:5:9: error[E0001]: undefined identifier `parse`
```

```loup
@mode(host.scripted)

from system.json import (parse, stringify);

let doc = parse("{\"name\": \"loup\", \"tags\": [\"a\", \"b\"]}").unwrap();
println(stringify(doc));
```

```text
{"name":"loup","tags":["a","b"]}
```

The namespaces `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 (§1-13.6). Your package cannot be named after one of them. `system` is not reserved. The Language Reference describes more modules than ship today — `sync.*`, `net.tls`, `core.reflect`, every `web.*` and `ui.*` module — and importing one gives an error like the one above.

## Projects

When a script grows into several files with tests, make it a project:

```sh
loup-bootstrap init notes
cd notes
```

(On macOS, Windows and Linux arm64, `loup init` is the same command; Chapter 1 explains the two binaries.) This creates:

```text
notes/
├── notes.lppkg     # the manifest
├── src/
│   └── main.loup   # the entry point
└── tests/
```

A project's entry point is a `host.dual` program with a `main()`, and its modules are imported by their path under `src/`. Add `src/text/slug.loup`:

```loup
@mode(host.dual)

## Lower-case `text` and join its words with hyphens.
pub func slugify(text: string): string {
    let words = text.trim().toLowerCase().split(" ");
    let kept = words.filter(func(w: string) => w.length() > 0);
    return kept.join("-");
}
```

*Listing 11-9: src/text/slug.loup*

and use it from `src/main.loup`:

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

from text.slug import (slugify);

func main(): int {
    for (title in runtime.args()) {
        println(slugify(title));
    }
    return 0;
}
```

*Listing 11-10: src/main.loup imports `text.slug` from `src/`*

```sh
loup run src/main.loup "Hello World" "  Error  Handling "
```

```text
hello-world
error-handling
```

`loup-bootstrap check`, run anywhere inside the project, type-checks every module without running anything and prints nothing when all is well. Library modules in a project are usually `host.dual` too, so the whole project can later be compiled ahead of time; a `host.dual` module may contain only declarations and constant initializers at the top level.

### The Manifest

The manifest is a TOML file named `<project>.lppkg` (or `package.lppkg`). `loup-bootstrap init` writes this, plus a number of empty tables:

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

[build]
entry = "src/main.loup"
delivery = "cli"
profile = "release"

[test]
pattern = "**/*_test.loup"

[dependencies]

[dev_dependencies]
```

| Key | Meaning |
|---|---|
| `name`, `version` | the package's name (lowercase, hyphens allowed, dots for namespaced packages) and its semantic version |
| `loup` | the minimum toolchain version |
| `[build] entry` | the entry module |
| `[build] delivery` | what `loup-bootstrap build` produces: `cli` (a WebAssembly module run by `loup-launcher`), `service`, `browser`, `desktop`, `native` (older manifests spell it `target`) |
| `[build] mode` | the default `@mode` for files that declare none |
| `[test]` | where `loup test` looks for tests (Chapter 16) |
| `[dependencies]`, `[dev_dependencies]` | packages the project needs |

The Language Reference's examples write `package my-app` and `[deps]` without TOML quoting; a manifest must be valid TOML, so use the form above.

## Sharing Code Between Projects

> ⚠ **Planned.** The package registry and the commands that use it — `loup add`, `loup publish`, a working `loup install`, and `loup.lock` — are described in the Packages Reference but not available in Loup 1.25. `loup add` and `loup publish` are unknown commands, and `loup install` stops as soon as the manifest lists a dependency:
>
> ```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 works today is `LOUPPATH`: a list of directories (separated by `:`, or `;` on Windows) that `loup run` searches for packages. Each entry is a *root* that contains packages; a package lives in a directory under it with its own manifest. With a `word-tools` package in `~/dev/packages/word_tools/` (a hyphen in a package name becomes `_` in its directory):

```text
~/dev/packages/
└── word_tools/
    ├── word-tools.lppkg   # name = "word-tools"
    └── count.loup         # pub func wordCount(…)
```

any script can import it:

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

from word_tools.count import (wordCount);

func main(): void {
    println(wordCount("one two three"));
}
```

```sh
LOUPPATH=~/dev/packages loup run src/main.loup
```

```text
3
```

Without `LOUPPATH`, the import fails with `unresolved package import 'word_tools/count'`, listing the roots it searched. In 1.25, `loup-bootstrap check` does not read `LOUPPATH`, so it reports such an import as `E-CHECK-DEP … not found`; use `loup run` to check a program that depends on one.

## Summary

- Every source file is a module, imported by its path relative to the importing script (or to `src/` in a project).
- `pub` exports a declaration; everything else is private to the file.
- `from m import (a, b as c);` always has parentheses; `import m as name;` binds a whole module; `from p { m import (…); }` groups imports. There are no wildcard imports and no `pub use` or `export`.
- `pub from .m import (…);` re-exports; an `index` file makes a directory importable.
- In 1.25, importing a module runs its declarations and `let` initializers but not its other top-level statements.
- Circular imports are `E_CIRCULAR_IMPORT`; `defer import` breaks a cycle between types.
- JSON is `system.json`; the reserved namespaces are the standard library's.
- `loup-bootstrap init` creates a project with a TOML `.lppkg` manifest and a `host.dual` entry point. The registry (`add`, `publish`, `install`) is planned; share local packages with `LOUPPATH`.

## Try It Yourself

1. Split Listing 10-9 (the inventory parser) into `inventory/parse.lpscr` with `pub func parseInventory` and a `report.lpscr` that imports it.
2. Add `inventory/index.lpscr` that re-exports `parseInventory` and `Item`, and change `report.lpscr` to import from `inventory`.
3. Make two modules import each other and read the diagnostic. Then fix it by moving the shared struct into a third module.
4. Create a project with `loup-bootstrap init`, move the inventory code under `src/`, and run `loup-bootstrap check`.
