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

std is a reserved toolchain namespace. The embedded modules std.math, std.path and std.fs are available without local sources. Unknown std modules never fall back to the source root. Local files/directories and declarations named std, and imports binding a non-standard module as std, are rejected.

import package.module
import package.module as module_alias
from package.module import Name as Alias

Comma-separated imports are supported. Imports occur only at module scope and execute no initializers. Relative imports, wildcards, re-exports, and implicit transitive imports are unsupported. Directories are namespaces, without __init__ execution.

The source root defaults to the entry file’s canonical directory; --module-root DIR selects another root. In a package, the root is src/, and the first component of an import may instead be a declared dependency alias, which names that dependency’s library; an alias may not match a local top-level module. a.b resolves to <root>/a/b.plenty or the trusted interface <root>/a/b.plentyi. Having both is ambiguous. Imported canonical filenames and directories must have identifier components and cannot escape the root. File/directory name collisions and import cycles are errors; a cycle diagnostic names the actual dependency chain.

import a.b binds the path a.b, and a.b.f names a member of that module. A binding cannot reuse a name the file declares or imports with from, and an alias cannot enclose another binding: import x as a with import a.b is rejected. Because a.plenty beside a/ is ambiguous, only a dependency alias names a module with other modules beneath it: tz names the library, such as src/lib.plenty, and tz.zones names src/zones.plenty of that dependency. A file can import both under their own paths, in either order:

import tz
import tz.zones

tz.f then names the library’s f, and tz.zones.f the f of tz.zones. If the library declares a member zones, public or private, tz.zones would name both, so the later import is rejected with a diagnostic naming both imports; import tz.zones as zones binds the module without the clash. The same holds for a deeper path such as tz.rules.dst and a library member rules. from tz import zones imports only a declaration of the library, never the module beside it, since imports do not re-export. When the library declares no zones and tz.zones is a module, the diagnostic says that from imports declarations and suggests import tz.zones or import tz.zones as zones. The same applies to a local namespace directory (from util import helpers) and to std (from std import json).

A module path is not a value. Using tz.zones (or tz) in an expression or type reports that it is a module and names one of its declarations, such as tz.zones.offset; a path that encloses an imported module, such as tz.rules with import tz.rules.dst, is reported as a module namespace. A missing member of an imported module, such as tz.missing, reports that it is private or is not a declared export.

Canonical paths identify modules, so imports through different aliases or diamond dependencies preserve the same nominal types. Private declarations remain private to their defining module, regardless of directory ancestry. All reachable modules are checked before execution. Imported modules need no entrypoint, and their main functions are ordinary functions.

The concurrency standard modules export existing builtin identities. An explicit import such as from std.thread import spawn may bind the matching builtin name; an import cannot replace a builtin with a different symbol. Item and module aliases retain the same types and compiler safety checks.

Constants

const name: T = value declares an immutable value at module scope; pub const exports it. T must be a concrete integer or float type, an integer domain such as Positive[i64], a Quantity[T, D], bool, str, or bytes. The compiler evaluates the initializer, a constant expression, and each read uses the result:

const KIB: i64 = 1 << 10
const MIB: i64 = KIB * KIB
const MASK: u8 = ~(1u8 << 3)
const RATIO: f32 = f32(MIB) / 3.0
const NAME: str = "plenty" + "-" + "lang"
const WIDE: bool = MIB > KIB and not (KIB == 0)

def main() -> Result[(), IoError]:
    print(MIB)?
    print(MASK)?
    print(RATIO)?
    print(NAME)?
    print(WIDE)?
    Ok(())
1048576
247
349525.34
plenty-lang
True

A constant expression contains literals, other constants (from this module or imported, in any declaration order), numeric type constants such as u8.MAX or f64.PI, numeric casts such as f32(x) and u8(x), parentheses, the unary operators -, ~, and not, the arithmetic, bitwise, shift, comparison, and logical operators, x if c else y, and + on two str values. Quantity construction, quantity operators, .get() and floating .to(unit) also fold. User function calls, f-strings, collections, and other expressions are rejected with “not a constant expression”.

Evaluation follows the numeric rules of the same expression at run time, so the folded value is the value the program would compute: // floors, % takes the divisor’s sign, / divides floats only, << discards shifted-out bits, >> is arithmetic for signed types, and operands must share a type. Unsuffixed literals take the type the context expects. Where run time would trap, evaluation is a compile error instead: overflow, division by zero, a shift count outside 0..width, and an out-of-range float-to-integer cast. As at run time, x if c else y evaluates only the selected branch, and and and or evaluate their right operand only when it decides the result; the other operand is still type-checked, so 1 if D == 0 else 100 // D is valid when D is zero. A cycle is reported with its path, for example constant A depends on itself: A -> B -> A.

An integer domain constant (NonZero[T], Positive[T] or NonNegative[T]) evaluates its initializer as T, and a value outside the domain is a compile error. Each read is the domain value, as if the literal were written there. Another initializer reads its integer with .get(), or names it alone as the whole initializer of a constant of the same type:

const MAX_URL: NonNegative[i64] = 8192
const PAGE: Positive[i64] = MAX_URL.get() // 64
const FIRST: Positive[i64] = PAGE
assert PAGE.get() <= 1024

def pages(size: Positive[i64] = PAGE) -> i64:
    MAX_URL.get() // size

def main() -> Result[(), IoError]:
    print(pages(), pages(FIRST), MAX_URL)?
    Ok(())
64 64 8192

Static assertions

assert CONDITION or assert CONDITION, "message" at module scope checks a constant expression during compilation. A false condition fails the build with static assertion failed, followed by the message when given. The message is itself a constant str expression. An assertion declares no name and produces no code.

const PAGE: i64 = 4096
assert PAGE % 64 == 0, "pages hold whole cache lines"

def main() -> Result[(), IoError | OverflowError[i64] | ZeroDivisionError]:
    print(PAGE // 64)?
    Ok(())
64

Module-scope assert uses the spelling of a Python assert statement, which the compiler checks once instead of at run time. Plenty has no run-time assert statement; inside a function, assert is an ordinary name.

Included files

include_str("path") initializes a str constant and include_bytes("path") a bytes constant with a file’s contents:

const SCHEMA: str = include_str("sql/schema.sql")
const ICON: bytes = include_bytes("assets/icon.png")

The argument is one string literal: a path relative to the directory of the source file that contains it. Absolute paths and paths that resolve outside the package’s source root, including through .. or symbolic links, are errors. include_str rejects a file that is not valid UTF-8 and names the first invalid byte. Both forms initialize a const only and may appear in other constant expressions, for example "> " + SCHEMA. They require a source file, so source passed without a path, such as an editor buffer, cannot use them.

The contents become read-only data in the executable; reads allocate nothing. An included file is an input of the program cache: editing it rebuilds the program.

Binding and representation

Imports and aliases bind constants as values: import std.math as math permits math.pi, while from std.math import pi permits pi. Constants are not callables or mutable places; brackets on a constant index it, as in TABLE[n] or tables.TABLE[n:]. Private constants follow the same visibility rules as private functions. A local binding may shadow an imported constant. Constant names share the declaration namespace with functions and types.

Constants require no module initialization or allocation. Each read of a numeric or bool constant compiles to an immediate of the constant’s declared type (an integer domain constant’s immediate is converted as a literal is), and each read of a str or bytes constant to a reference to read-only data, in debug and release builds; there is no getter function, global storage, or code run at import time. See call-site substitution.