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

Visibility

Visibility decides which declarations other files may use. A top-level declaration has one of three levels:

WrittenVisible to
private f()its own file only
f()the files of its module
export f()any file that imports it

With no modifier, a declaration is internal to its module: the files of one module see each other’s declarations, and nothing else does. A file outside any module is a module of its own, so its declarations are private to the file. This lets a module, or a file, keep its helpers to itself and offer a small, clear surface to the code that imports it.

private is optional. It is recommended for a helper that only its own file uses, so it does not leak into the rest of the module, and it is needed only where two files of one module would otherwise declare the same name (see Keeping a declaration to its file).

Exporting declarations

Put export in front of a declaration to make it usable from other files that import this one (see Modules and imports for how to import). You can export every kind of top-level declaration:

  • functions, including extern declarations of C functions
  • structs
  • enums
  • type aliases
  • traits
  • extensions
  • module-scope variables, which are always const and thread-safe (see Module-scope variables)

Here is a library file with a mix of exported and private declarations, and a program that uses it:

import async;

# Everything here is private to lib.pep unless it says `export`.

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

export type Points = Point[];

export trait Describe {
    describe(): String;
}

# A module-scope variable is `const` and thread-safe: change it through
# an Atomic.
export const points_made = async.Atomic<int>(0);

export origin(): Point {
    return make(0, 0);
}

export at(x: int, y: int): Point {
    return make(x, y);
}

# Private: only lib.pep can call make().
make(x: int, y: int): Point {
    var p = Point();
    p.x = x;
    p.y = y;
    points_made.fetch_add(1);
    return p;
}

# An exported extension: its methods are usable wherever Point is.
export extend Point with Describe {
    describe(): String {
        return "point";
    }
}

# A private extension: only lib.pep can call p.norm().
extend Point {
    norm(): int {
        return x * x + y * y;
    }
}

export norm_of(p: Point): int {
    return p.norm();
}

# `export` goes after attributes, directly before the declaration.
[Target(OS(Windows))]
export platform_name(): String { return "Windows"; }

[Target(!OS(Windows))]
export platform_name(): String { return "not Windows"; }
import "lib";

main(): int {
    var p = at(3, 4);             # exported function
    var o: Point = origin();      # exported struct, used as a type
    Points list = Points();       # exported type alias
    list.append(p);
    list.append(o);

    assert(p.x == 3, "fields are as visible as their struct");
    assert(p.describe() == "point", "method from an exported extension");
    assert(norm_of(p) == 25, "an exported function may use private helpers");
    assert(points_made.load() == 2, "exported module variable");
    assert(platform_name().length > 0, "exported, with an attribute");
    return 0;
}

main.pep can call at, origin, and norm_of, use Point and Points as types, and read the counter in points_made, an exported const Atomic<int>. It cannot use make or call p.norm(). Trying to is an error that tells you how to fix it:

import "lib";

main(): int {
    var q = make(1, 2);
    return 0;
}
main.pep:4:13: error: 'make' is private to lib.pep; declare it with 'export' to use it from another file
    4 |     var q = make(1, 2);
      |             ^~~~
lib.pep:29:1: note: declared here
   29 | make(x: int, y: int): Point {
      | ^~~~

The same applies to private structs, enums, type aliases, traits, and variables: naming one from another file reports 'name' is private to file.pep.

Files of a module see each other through imports

Inside a module, export is not needed for one file to use another’s declarations, but an import is: a file sees the top-level declarations of another file of its module, exported or not, once it imports that file (import "rules";), as a C file sees another’s declarations through its header. export means visible outside the module, through its module.pep.

Here the inventory module keeps its rules in rules.pep, which exports nothing, and stock.pep imports it to use them:

# Module-internal: no `export`, yet every file of `inventory` can use these.
const RESTOCK_LEVEL = 5;

struct Shelf {
    var count: int;
}

needs_restock(shelf: Shelf): bool {
    return shelf.count < RESTOCK_LEVEL;
}
# Uses `Shelf`, `needs_restock` and `RESTOCK_LEVEL` from rules.pep, which
# this file imports: a file of a module sees another through an import.
import "rules";

export struct Stock {
    var _shelf: Shelf;

    construct(count: int) {
        _shelf = Shelf();
        _shelf.count = count;
    }

    low(): bool {
        return needs_restock(_shelf);
    }

    missing(): int {
        if low() {
            return RESTOCK_LEVEL - _shelf.count;
        }
        return 0;
    }
}
# The module's interface: only what this file exports reaches importers.
export * from "stock";

# `rules.pep` exports nothing, so importing it only makes it part of the
# module.
import "rules";
import "inventory";

main(): int {
    var s = Stock(2);
    assert(s.low(), "two is below the restock level");
    assert(s.missing() == 3, "three more reach it");
    # `needs_restock(...)` here would be an error: it is internal to the
    # module 'inventory'.
    return 0;
}

A file’s own declarations come first, then what its imports bring in (the files of its module it imports included), then the runtime’s. A file of the module that names a declaration of another one it does not import gets an error that names the file to import. Without its import "rules";, stock.pep would report:

inventory/stock.pep:5:17: error: 'Shelf' is declared in inventory/rules.pep, another file of this module, which this file does not import; add an import for it
    5 |     var _shelf: Shelf;
      |                 ^~~~~

So a file’s dependencies are exactly its imports, and the compiler checks and caches each file against what it imports alone. A module’s files are the files of the program in its folder (and in subfolders that are not modules themselves): a file joins the program when the module imports it, so module.pep imports a file with nothing to export, like rules.pep, just to include it. A [Target(...)] on that import (see Platform-specific imports) includes the file only on some platforms.

Outside the module, its internal declarations stay hidden. Using one is an error that names the module:

main.pep:3:13: error: 'Shelf' is internal to module 'inventory'; export it from the module to use it here
    3 |     var s = Shelf();
      |             ^~~~~
inventory/rules.pep:4:8: note: declared here
    4 | struct Shelf {
      |        ^~~~~

The files of a module still share one set of names, so two of them may not both declare a struct, enum, type alias, trait, or variable of one name: the second is a redefinition of 'name', with a note at the first, unless one of them is private. Functions are different: functions of one name in several files of a module are one overload set, as they are within a file. Extensions are shared the same way: a file sees the methods the files of its module it imports add, exported or not, unless the extension is private.

Keeping a declaration to its file

Put private in front of a top-level declaration to keep it to its own file: the other files of its module do not see it. You can mark every kind of declaration that export applies to. Two files of one module may then each declare a private helper of the same name, and each file’s code reaches its own. Here summary.pep and detail.pep each have a private WIDTH and label, and share the module-internal pad:

# `private` keeps these to this file: detail.pep may declare its own
# `WIDTH` and `label` without a clash.
private const WIDTH: ulong = 10;

private label(count: int): String {
    return $"{count} item(s)";
}

# Module-internal: no modifier, so detail.pep can use it too.
pad(text: String, width: ulong): String {
    if (text.size >= width) {
        return text;
    }
    return text + " ".repeat(width - text.size);
}

export summary_line(count: int): String {
    return pad(label(count), WIDTH);
}
# The same names as summary.pep's private ones, and private here too.
import "summary";

private const WIDTH: ulong = 12;

private label(name: String): String {
    return $"- {name}";
}

export detail_line(name: String): String {
    return pad(label(name), WIDTH) + "|";
}
# The module's interface: one function from each file.
export * from "summary";
export * from "detail";
import "report";

main(): int {
    assert(summary_line(3) == "3 item(s) ", "summary.pep's own label and width");
    assert(detail_line("pen") == "- pen       |", "detail.pep's own label and width");
    # `label(3)` here would be an error: it is private to summary.pep.
    return 0;
}

A private declaration stays out of the rest of the module entirely:

  • Another file of the module that names it, without a declaration of its own by that name, gets an error saying to remove private. Were detail.pep to have no label of its own, calling label(3) there would report:

    report/detail.pep:6:16: error: 'label' is private to report/summary.pep; remove 'private' to use it from the other files of its module
        6 |     return pad(label(3), WIDTH) + "|";
          |                ^~~~~
    report/summary.pep:5:9: note: declared here
        5 | private label(count: int): String {
          |         ^~~~~
    
  • A private function joins no overload set of the module: functions of one name in the other files form their set without it, and its own file sees only its own.

  • A private extension’s methods are usable only in its file.

Code outside the module never sees a private declaration, as it never sees a module-internal one ('label' is internal to module 'report').

In a file outside any module, private changes nothing: its declarations are already private to it. A declaration cannot be both export and private:

main.pep:1:8: error: a declaration cannot be both 'export' and 'private'
    1 | export private f(): int { return 0; }
      |        ^~~~~~~

Members of structs and extensions

Fields and methods declared inside a struct’s body are reached through the struct, so they share the struct’s visibility: if you can use the struct, you can use all of its members.

To let other code read a value but not write it, make it a property with only a get: var balance: long { get; } can be assigned only by the struct’s own constructors and methods.

Methods added by an extension share the extension’s visibility instead. In the example, norm() is added by a private extension, so only lib.pep (or, in a module, the module’s files) can call it, even though Point itself is exported:

main.pep:4:14: error: 'norm' is declared in a private extension in lib.pep; declare the extension with 'export' to use it from another file
    4 |     return p.norm();
      |              ^~~~
lib.pep:46:5: note: declared here
   46 |     norm(): int {
      |     ^~~~

An exported extension’s methods are usable where its file is imported, not everywhere: a file sees the extensions it declares itself, the exported extensions of the files it imports, the exported extensions of every file of a module it imports (import geo reaches geo/ext.pep’s), and those of the runtime, which every file sees. Here measure.pep adds manhattan() to Point, and main.pep imports it to call it:

import "point";

# An exported extension: files that import measure.pep can call these.
export extend Point {
    manhattan(): int {
        return abs(x) + abs(y);
    }
}

# Private to this file.
abs(v: int): int {
    if (v < 0) { return -v; }
    return v;
}
import "point";
import "measure";                      # brings Point's manhattan() into scope

main(): int {
    var p = Point();
    p.x = 3;
    p.y = -4;
    assert(p.manhattan() == 7, "an extension from an imported file");
    return 0;
}

Without import "measure";, the call is an error. If no file of the program imports measure.pep, the method does not exist at all (no member named 'manhattan' in 'Point'); if another file does, the error says which import is missing:

main.pep:8:14: error: 'manhattan' is declared in an extension in measure.pep, which this file does not import; add an import for it
    8 |     assert(p.manhattan() == 7, "an extension from an imported file");
      |              ^~~~~~~~~
measure.pep:5:5: note: declared here
    5 |     manhattan(): int {
      |     ^~~~~~~~~

Two kinds of call are checked where they take effect rather than here:

  • A method of a trait you can see (s.area() where area is a method of a trait Shape you declare, import, or get from the standard library): the call may go through a value of that trait, which works whatever file declares the conformance. A file can return a Shape whose struct and conformance stay private, and callers still call area on it.
  • A call on a receiver whose type is not known in this file: this.m() in a trait’s default method, or x.m() (or x.f().m()) where x is a parameter typed by a type parameter, the function’s or its struct’s (measure<X>(x: X)). Such a call resolves where the generic is used, with that file’s imports.

When the extension is in a file of another module, the error names the module to import instead (import its module 'geo'), since a file of another module is reached only through its module.

Inside a struct’s methods (and a trait’s default methods), a bare call to a name that an extension you cannot see adds as a method is an error too, rather than a call to the top-level function of that name: the compiler would otherwise bind it to the method. Rename one of them, or import the extension’s file if the method is what you meant. That import may point back at a file that imports this one (a struct’s file importing the file that extends it); such cycles are allowed.

Extending a struct does not make a private struct visible to other files.

Visibility is checked before types are known, so members are matched by name: a call x.m() is accepted if any struct body visible to the file (its own, or exported) or any extension visible to it, as above, declares a member named m.

Where export and private go

export and private go directly before the declaration’s keyword or name, after any attributes:

[Target(OS(Windows))]
export platform_name(): String { return "Windows"; }

[Inline]
private twice(x: int): int { return x * 2; }

Writing it before the attributes is a parse error:

main.pep:2:8: error: unexpected '['
    2 | export [Target(OS(Windows))] f(): int { return 1; }
      |        ^

export controls visibility between Pepper files only. Making a function callable from C with C linkage is a separate matter, handled by the [Export] attribute described in Calling Pepper from C.

The runtime’s exports

The runtime’s public surface (String, Array, HashMap, HashSet, Result, Error, hash, assert, panic, and the rest of the standard library reference) is exported the same way, and is visible in every file without an import. The runtime’s own helpers are private, so calling one, such as _pep_check_sync_enabled, reports '_pep_check_sync_enabled' is private to followed by the path of the runtime’s internals.pep.

Each module has its own namespace

A private declaration belongs to its file (or its module) alone, so two files outside any module, or two modules, may each declare a private name without clashing. Within a module, private does the same for two of its files (see Keeping a declaration to its file). Here main.pep and lib.pep each have their own Counter and make, and each file’s code reaches its own:

# lib.pep's private helpers: no other file can see them, and no other
# file's names can clash with them.
struct Counter { var n: int; }

make(): Counter {
    var c = Counter();
    c.n = 10;
    return c;
}

export lib_total(): int {
    return make().n;
}
import "lib";

# main.pep has its own private Counter and make(), unrelated to lib.pep's.
struct Counter { var n: int; var step: int; }

make(): Counter {
    var c = Counter();
    c.n = 1;
    c.step = 2;
    return c;
}

main(): int {
    var c = make();                    # main.pep's make()
    assert(c.n + c.step == 3, "main's own Counter");
    assert(lib_total() == 10, "lib's make() is its own");
    return 0;
}

This holds across modules too: two modules may each have a util.pep with a private helper. Files are told apart by their path inside their module (io/file.pep), not by their file name alone. Within one module, the files share their names instead, except private ones (see Files of a module see each other through imports).

Exported names work the same way between modules: each module’s exports are its own, so two modules may export the same name, and importers tell them apart by qualifier (a.parse and b.parse) or by an import list (import [parse as parse_b] from b). Using a name that two whole-file imports both provide is an error at the use (see Modules and imports). A private name may repeat an exported one from another file; in the file that declares it, the file’s own declaration is the one that name refers to.

Two kinds of file keep their exported names program-wide, because every file reaches them without an import or by its bare name: the runtime (and any --import module), and the program’s main file. So an export may not repeat a runtime name: exported 'String' is also declared in followed by the runtime’s file.

A few kinds of declaration are not private in this sense:

  • extern declarations name a C function, which is the same function in every file. Several files may declare the same extern.
  • Concrete overrides of a generic function, such as hash<int> alongside hash<T>, take the generic function’s name: they replace its version for those types wherever it is called. So each override may be declared in only one file (override 'hash<int>' is also declared in b.pep), and overriding one the runtime already provides, such as hash<String>, is also an error. An override must be for a generic function you can see: overriding another file’s private generic function reports that it is private. An override may be for a struct, such as hash<Point>, and must be declared in the file or module that declares the generic or one of its type arguments (see Generics).
  • Declarations that a [Target(...)] attribute drops for the platform being compiled (see Platform selection) are never compiled. This is why the first lib.pep above can declare platform_name twice.