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.