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:
| Form | What 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:
| Form | What 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 ... fromthat adds the second one. List the names you want instead, or rename one withas. 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 ... fromlines 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 asu.BASE), in types (var p: g.Point,g.Box<int>[], a return typeg.Point), and in generic constructions (g.Box<int>()). A generic construction is recognized only for a qualifier imported earlier in the file; elsewherea.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_areaisshapes.pep’sarea, callingg.rect_area(1, 2)inside a function with a parameter namedareacallsshapes.pep’s function. The one exception is a re-exported runtime name: if a module exports the runtime’sprintassay, a local namedprintwhere you callsayis 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";