Visibility
Visibility decides which declarations other files may use. A top-level declaration has one of three levels:
| Written | Visible 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
externdeclarations of C functions - structs
- enums
- type aliases
- traits
- extensions
- module-scope variables, which are always
constand 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. Weredetail.pepto have nolabelof its own, callinglabel(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
privatefunction 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
privateextension’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()whereareais a method of a traitShapeyou 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 aShapewhose struct and conformance stay private, and callers still callareaon it. - A call on a receiver whose type is not known in this file:
this.m()in a trait’s default method, orx.m()(orx.f().m()) wherexis 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:
externdeclarations 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>alongsidehash<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 ashash<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 ashash<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 firstlib.pepabove can declareplatform_nametwice.