Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Modules and imports

As a program grows, you split it into files, and group related files into modules. An import makes another file’s or module’s exported declarations available in the importing file. What a file exports is covered in Visibility.

Importing a file

The simplest import names another file, as a string, relative to the importing file. The .pep extension is optional:

# Every import comes before the file's declarations.
import "util";                       # util.pep's exports, unqualified
import "util" as u;                  # the same exports, as u.name
import [thrice as triple] from "util";
import "helpers/text";               # a file in a subfolder

main(): int {
    assert(twice(BASE) == 20, "unqualified names from util.pep");
    assert(u.thrice(2) == 6, "qualified by the alias");
    assert(triple(3) == 9, "renamed by the import list");
    assert(shout("hi") == "hi!", "from helpers/text.pep");

    # An imported function is a value like any other.
    var f = u.twice;
    assert(f(4) == 8, "function reference through a qualifier");
    return 0;
}

util.pep sits next to main.pep, and text.pep is in a helpers folder:

export const BASE = 10;

export twice(n: int): int {
    return n * 2;
}

export thrice(n: int): int {
    return n * 3;
}
export shout(s: String): String {
    return s + "!";
}

An import comes in one of these forms:

FormWhat it makes available
import "util";Every export of util.pep, unqualified: twice(BASE).
import "util" as u;Every export, qualified by the name you choose: u.twice(1).
import [thrice as triple, twice] from "util";Only the listed exports, unqualified, each under its own name or the one after as.
import geometry;A module’s exports, qualified by the module’s name: geometry.Point.
import geometry as g;A module’s exports, qualified by g.
import [area, Point as Pt] from geometry;The listed exports of a module, unqualified.

An import ends with a semicolon.

An import binds names only in the file that contains it. The exports of a file that this file did not import are not visible to it, even when another file of the program imports them. Here geometry/module.pep imports shapes.pep, but main.pep imports only the module:

import geometry;
main(): int { return area(1, 2); }
main.pep:2:22: error: 'area' is exported by deps/geometry/shapes.pep, which this file does not import; add an import for it
    2 | main(): int { return area(1, 2); }
      |                      ^~~~
deps/geometry/shapes.pep:3:8: note: declared here
    3 | export area(w: int, h: int): int {
      |        ^~~~

Imports come first

Every import, and every export ... from, comes before all of a file’s declarations and export name re-exports:

main(): int { return 0; }
import geometry;
main.pep:2:1: error: imports must come before all declarations
    2 | import geometry;
      | ^~~~~~

Modules

A module is a folder that contains a module.pep file. The module is named after its folder, and its exports are exactly what its module.pep exports. The other files in the folder are the module’s own files: module.pep imports them and chooses what to pass on to the module’s users. A file of a module sees the declarations of the module’s files it imports, exported or not, except those marked private (see Files of a module see each other through imports). Loading any file of a module loads its module.pep, and with it every file the module imports.

This example has a geometry module next to main.pep:

modules-folders/
    main.pep
    geometry/
        module.pep      # the module's interface
        shapes.pep      # a file of the module
        solids/
            module.pep  # a module nested inside geometry
# A file of the geometry module. Its exports are visible to the files of
# the module that import it, not to the module's importers.
export area(w: int, h: int): int {
    return w * h;
}

export struct Box<T> {
    var value: T;
}
# A module nested in the geometry folder.
export volume(w: int, h: int, d: int): int {
    return w * h * d;
}
# The geometry module: what this file exports is the module's interface.
import "shapes";
import "solids" as solids;

export struct Point {
    var x: int;
    var y: int;
}

export make(x: int, y: int): Point {
    var p = Point();
    p.x = x;
    p.y = y;
    return p;
}

reset_count(): int {
    return 0;
}

export area as rect_area;           # shapes.pep's area, renamed
export Box;                         # shapes.pep's Box, under its own name
export reset_count as reset;        # a private function, exported as reset
export solids.volume as box_volume; # from the nested module
import "geometry" as g;
import [rect_area, Point as Pt] from "geometry";

# Qualified names work in types, too.
corner(): g.Point {
    return g.make(1, 1);
}

main(): int {
    var p: g.Point = g.make(3, 4);
    assert(p.x == 3, "qualified function and type");

    Pt q = corner();
    assert(q.y == 1, "the struct under its import-list name");

    assert(g.rect_area(2, 3) == 6, "re-exported under a new name");
    assert(rect_area(2, 5) == 10, "imported unqualified");
    assert(g.reset() == 0, "a private function, re-exported");
    assert(g.box_volume(2, 3, 4) == 24, "re-exported from a nested module");

    var b = g.Box<int>();
    b.value = 7;
    var boxes: g.Box<int>[] = g.Box<int>[]();
    boxes.append(b);
    assert(boxes[0].value == 7, "a generic struct through a qualifier");
    return 0;
}

main.pep imports the folder with a string, import "geometry", because the module lives next to it. A string import names either a file (geometry.pep) or a module folder (geometry/) relative to the importing file. A module imports a module nested in its own folder the same way: geometry/module.pep imports "solids".

Re-exports

export name as new_name; exports name to importers under new_name, and export name; exports it under its own name. The name may be declared in the file (even privately) or imported from another file or module, qualified or not. This is how a module.pep publishes what its other files define.

The semicolon is required. Without it, export Box followed by a declaration would read as an exported C-style declaration whose type is Box.

The exports of the module’s other files are not the module’s exports. In the example, users of geometry can call g.rect_area, but not g.area:

main.pep:2:22: error: 'area' is not exported by 'geometry' (deps/geometry/module.pep)
    2 | main(): int { return geometry.area(1, 2); }
      |                      ^~~~~~~~~~~~~

Re-exporting a whole file or module

A module.pep that only passes on its files’ exports would repeat every name. export ... from passes them on in one line, in the same shapes as the import ... from forms:

FormWhat importers get
export * from "rect";Every export of rect.pep, each under its own name.
export * from geometry;Every export of the module geometry.
export [perimeter as outline, Side] from "measure";Only the listed exports, each under its own name or the one after as.
# The shapes module: its interface is its files' exports, passed on whole or
# by name.
export * from "rect";                               # every export of rect.pep
export [perimeter as outline, Side] from "measure"; # two, one renamed
import "shapes";

main(): int {
    var r = make_rect(3, 4);
    assert(area(r) == 12, "from export * of rect.pep");
    assert(outline(r) == 14, "from the export list, renamed");

    var s = Side();
    s.length = 5;
    assert(s.length == 5, "a struct from the export list");
    return 0;
}
  • It re-exports; it doesn’t import. export * from "rect"; makes no name visible in the file that contains it. Inside a module that does not matter, since the module’s files see each other’s declarations anyway; a file outside it imports the names as usual (import "rect";).

  • Only exports travel. Private declarations stay behind, and extensions stay visible only as they always are.

  • Collisions are errors. If two re-exports, or a re-export and one of the file’s own exports, publish one name for different declarations, the compiler reports it at the export ... from that adds the second one. List the names you want instead, or rename one with as. The exception is functions declared under one name: they join one overload set. Anything else collides:

    shapes/module.pep:2:1: error: 'area' is exported twice by module 'shapes'
        2 | export * from "measure";
          | ^~~~~~~~~~~~~~~~~~~~~~~
    shapes/module.pep:1:1: note: also exported here
        1 | export * from "rect";
          | ^~~~~~~~~~~~~~~~~~~~
    
  • Like imports, export ... from lines come before every declaration and take attributes such as [Target(...)]. A deprecated declaration stays deprecated through every re-export.

A module’s files stay inside it

A string import from a file of a module must name a file of the same module, or a module folder. Reaching into another module’s files, or out of your own module, is an error:

main.pep:1:8: error: "geometry/shapes.pep" belongs to another module; import that module's folder instead
    1 | import "geometry/shapes";
      |        ^~~~~~~~~~~~~~~~~
geometry/bad.pep:1:8: error: "../main.pep" is outside this file's module; a module imports only its own files
    1 | import "../main";
      |        ^~~~~~~~~

A file outside any module, such as your program’s main.pep, may import files in its own folder or below, except files inside a module folder there.

Files of one module may import each other, even in a cycle.

Dependencies and search paths

A module that is not part of your program’s own folders, such as a library you depend on, is imported by name rather than by string:

import geometry;

main(): int {
    var p: geometry.Point = geometry.make(3, 4);
    return p.x - 3;
}

An identifier import looks for a folder geometry/module.pep on the search paths: first each directory you pass with -I, in order, then the runtime directory. If geometry lives in a deps folder, compile with:

pepper build main.pep -I deps -o main

An identifier import never looks next to the importing file. A module that is not found is reported with the places the compiler looked:

main.pep:1:8: error: cannot find module 'nowhere': no nowhere/module.pep in deps, /home/you/pepper/runtime
    1 | import nowhere;
      |        ^~~~~~~

The standard library’s io module is found this way, in the runtime directory: import io; then io.File, or import [File, Path] from io;.

A module nested in another module’s folder is imported with a dotted name: import geometry.solids; finds geometry/solids/module.pep and binds its last name, so its declarations are solids.volume (or, with import geometry.solids as s;, s.volume).

Projects and packages

A project’s pep.toml lists the packages it depends on, and each key is the name source code imports:

[dependencies]
geometry = { github = "someone/geometry", version = "1.2" }

import geometry; then finds the package’s root module.pep in the project’s .pep/deps, with no -I, and import geometry.solids; the module in its solids folder. pepper build with no file builds the project. See Projects: pep.toml.

The runtime prelude and --import

The runtime itself is not a module: its files are loaded into every program, and their exports, such as String, Array, and Result, are visible everywhere without an import.

--import NAME adds a module, or a file NAME.pep, found on the search paths to that prelude, so every file sees its exports without importing it. A name that resolves to neither is an error:

error: --import no_such_module: cannot find module 'no_such_module' (no no_such_module/module.pep) or file no_such_module.pep in /home/you/pepper/runtime

A prelude module may redefine an export of the runtime, such as panic: it is loaded ahead of the runtime and its definition wins. It must declare the name itself (export panic(...)); re-exporting another function under a runtime export’s name is an error, since the runtime’s own files would keep calling their version. When the runtime’s function is overloaded, the prelude module replaces only the overloads it declares, matched by parameter types, defaulted ones included: an export assert(condition: bool, message: String, location: __ptr<char> = __caller_location()) replaces the String form, and a string literal message still calls the runtime’s __ptr<char> form.

Only functions may be redefined this way. A struct, trait, type alias or variable that a prelude module exports under a name the runtime (or another prelude module) already exports is an error in the module loaded first, with a note at the other:

alt/module.pep:1:15: error: redefinition of 'MapIterator'
    1 | export struct MapIterator<K, V> { var key: K; var value: V; }
      |               ^~~~~~~~~~~
runtime/iterator.pep:182:15: note: also exported here

How imported names behave

  • Qualified names work wherever a name does: in expressions (g.rect_area(1, 2), or a module variable such as u.BASE), in types (var p: g.Point, g.Box<int>[], a return type g.Point), and in generic constructions (g.Box<int>()). A generic construction is recognized only for a qualifier imported earlier in the file; elsewhere a.b < c, d > (e) is two comparisons.
  • Local names shadow imports. A local variable, parameter, generic parameter, or struct member hides a qualifier or an imported name of the same name.
  • A file may not import a name it also declares at top level: 'make' is imported but also declared in this file. Functions are the exception: a file’s own functions of a name join the imported ones in one overload set.
  • An import list wins. A name listed in import [...] takes priority over the same name from a whole-file import. When two whole-file imports provide the same name differently, using that name is an error because it is ambiguous, unless both are functions declared under that name, which join one overload set.
  • A local may share an imported declaration’s name. If g.rect_area is shapes.pep’s area, calling g.rect_area(1, 2) inside a function with a parameter named area calls shapes.pep’s function. The one exception is a re-exported runtime name: if a module exports the runtime’s print as say, a local named print where you call say is an error that asks you to rename one of them.

Each module (and each file outside a module) has its own namespace, for its exported declarations as well as its private ones, so modules may reuse names freely: two modules may both export parse, and a.parse and b.parse stay apart. See Each module has its own namespace. Here metric and imperial each export a Length, a parse and a unit:

# Both modules export `Length`, `parse` and `unit`; each keeps its own.
import "metric" as metric;
import "imperial" as imp;
import [unit as imperial_unit] from "imperial";

main(): int {
    var a: metric.Length = metric.parse("abc");
    var b: imp.Length = imp.parse("abc");
    assert(a.metres == 3.0, "metric.parse");
    assert(b.feet == 9.0, "imperial.parse");
    assert(metric.unit() == "m", "qualified by the alias");
    assert(imperial_unit() == "ft", "renamed by the import list");
    return 0;
}

Platform-specific imports

An import or a re-export may carry attributes like any declaration. With a [Target(...)] attribute it applies only on matching platforms and is ignored elsewhere (see Platform selection):

[Target(OS(Linux))]
import linux;

[Target(OS(Linux))]
export linux_open as open;

[Target(OS(Windows))]
export * from "windows";