Introduction
Pepper is a compiled programming language for writing fast, native programs that are safe by default. Like Rust and Swift, it aims to rule out whole classes of memory bugs without asking you to manage memory by hand. It puts three things first:
- Clarity. Code should say what it does. Pepper has a small, familiar syntax, one way to write most things, and error messages that point at the exact token you need to change.
- Simplicity. There is no inheritance, no garbage collector to tune, and no ownership annotations to learn. Structs hold data, methods and extensions add behavior, and traits describe what a type can do.
- Concurrent safety. Shared data should be safe to use from more than one
thread. Only thread-safe values can be shared: numbers,
async.Atomic<T>, and structs declared[Shared], whose fields the compiler checks.
Here is a complete Pepper program:
# Every program starts at main. Its return value is the process exit code.
main(): int {
print("Hello, world!");
return 0;
}
How Pepper manages memory
Every struct value in Pepper is a reference to an object on the heap, and the
object is freed when the last reference to it goes away. The model is exactly
C++’s std::shared_ptr<T> passed by value: storing a reference in a
variable, a field, an array element or a parameter adds a reference, and that
reference is released when the variable goes out of scope or is overwritten.
There is no separate notion of a “borrowed” reference to learn. Numbers and
other primitive values are copied, as in C.
Because the rules are the same everywhere, you can predict exactly when an object is freed and when its destructor runs. The chapter on memory and reference counting covers the model in detail.
How Pepper is built
The compiler is written in Pepper, and builds itself. It parses your program, checks it, and generates native code through LLVM, then links an executable with the system’s C toolchain. The same compiler powers a language server, so editors such as VS Code show Pepper’s errors as you type.
Pepper runs on Windows (x64), Linux (x64) and macOS (arm64), compiles
to WebAssembly for WASI (--target wasm32-wasip1, see
WebAssembly), and builds
libraries that iOS and Android apps call (see
iOS and Android).
Each release is prebuilt for Windows (x64), Linux (x64, glibc 2.28 or newer)
and macOS (arm64 and x64), and installs with one command: see
Installing Pepper.
Current status
Pepper is young. The core language works and is tested: variables, functions,
structs with constructors, destructors, methods and properties, value structs and enums ([Value]), derived == and hash ([Equatable]), enums (with cases that carry values, methods and computed properties) and match (as a statement or a value, on enums, integers, characters, strings and trait values, with is tests, nested and tuple patterns, guards, ranges and constants), tuples
and multiple return values, array and map literals, generics (with type
arguments inferred at calls, and generic methods), type aliases,
traits (with default methods, and usable as types that convert back with as?), function types and
closures, modules (with export * from re-exports), Result-based error handling (Result is an enum, and ? propagates errors), Optional values, iteration (with map,
filter, enumerate and zip adapters),
with blocks, weak references (to structs and trait values), and calling C (including C callbacks), and being called from C ([Export]ed functions in a static or shared library, with a generated header). The standard library covers strings (with a StringBuilder, StringConstant for literals’ constant bytes,
to_string and a ToString trait, $"{a + b:>8}" interpolation with format specifications, and String.format), a Comparable trait for ordered types, sortable, searchable arrays with read-only slices, hash
maps and sets, sorted maps and sets (Map, Set) and an insertion-ordered OrderedMap, a BitSet of small integers, a double-ended Deque and a PriorityQueue, atomics, threads, channels and mutexes, tasks (green threads
that take turns on a few worker threads, parking on sockets and timeouts), file-system access (see files), text encodings (UTF-16, ASCII; see text),
TCP, UDP and Unix domain sockets, TLS, an HTTP/1.1 and HTTP/2 client and server (streamed bodies, redirects, connection reuse, ALPN), URI parsing and percent-encoding, base64, form data and UUIDs, regular expressions, JSON (parsing and writing JsonValues, JSONC documents that keep their comments, and converting structs with json.encode and json.decode), TOML (parsing, writing, editing a document in place, and converting structs), gzip and zlib compression, tar archives, cryptographic hashes (SHA-256, SHA-1 and BLAKE3 in the digest module), random numbers (secure bytes from the operating system and a seedable generator), SIMD vectors (simd.Vector<T, N>), command-line arguments (main(args: String[]), parsed into options with the cli module), starting child processes, the os module (environment variables, monotonic and wall clocks with Duration, and machine and user information), the time module (dates, times of day, time zones from the system’s database, and RFC 3339, HTTP and strftime text), the debug module (stack backtraces, which a panic also prints, and the debugger), and leveled, structured logging (the log module). Unit tests are [Test] functions that
pepper test runs in parallel, each in a process of its own. That is enough to write real
programs, but expect gaps. pep-fmt, the code formatter,
pep-profile, which profiles a program and draws a
flame graph, pep-test, which runs unit tests, and
pepper, the entry point that runs every
tool, are themselves written in Pepper. A pep.toml
project builds with pepper build alone and imports the packages it depends
on by name, which pepper package fetches from
GitHub, GitLab or Codeberg.
Some things you will meet quickly:
- Functions are values: a named function, an
externC function, a method (p.norm,Point.norm), a constructor (Point), a generic function’s specialization (larger<int>) or a closure such as(x: int) => x * 2can be stored, passed and called through a function type such as(int) => int(see function types and closures). - A constructor marked
[Implicit]converts a value into its struct wherever the struct is expected, soio.Pathparameters accept aString(see implicit conversions). - Free functions, methods, operators, constructors, extension methods and trait members can be overloaded.
- A
staticblock runs in the compiler: it can list a struct’s fields and the attributes on them ([Attribute]structs of your own) and generate code for each, so one function can encode any struct (see reflection and attributes). -D NAME=VALUEon the command line defines a build option, which[BuildOption("NAME")]and[Target(BuildOption(NAME) >= 2)]select code by andstaticblocks read (see build options).- A call can name its arguments,
window("Editor", resizable = false), to skip defaulted parameters or pass them in any order (see named arguments). - Generic parameters can require traits (
<T: Shape & Named>), checked wherever the generic is used and, inside the generic, against what the traits declare (see constraints). Numbers,booland enums whose cases carry nothing can declare traits too, for constraints, but are never trait values. - Threads can share only thread-safe values, which the compiler checks,
closure captures included. A channel hands over what it is sent without
copying it (
send(move(x)), orsend(copy(x))to keepx), aSelectwaits to receive from or send to several channels at once, and aMutexholds any struct, with the compiler keeping each locked value apart from the rest of the program (see threads). AnRwLocklets many threads read a thread-safe value at once, andSemaphore,BarrierandLatchcoordinate groups of threads. A[ThreadLocal]module-scopevargives every thread its own copy, compiled to the platform’s native thread-local storage (see thread-local variables). - Integer arithmetic that overflows panics instead of wrapping, at every
optimization level;
wrapping_addand its siblings wrap on purpose (see integer overflow). Bit counting, rotation, byte swapping and the floating-point functions are methods that compile to LLVM’s intrinsics, and any other intrinsic can be declared as an extern (see LLVM intrinsics). - Reference counting does not reclaim cycles on its own: two objects that refer to each other are never freed. Break the cycle with a weak reference on one side.
Each chapter of this book says plainly what is and is not supported. When a chapter shows a feature, the example was compiled and run to check it.
How this book is organized
- Tutorial. Getting started builds the compiler and runs your first program. A tour of Pepper then walks through the main features by building a small word-counting program.
- Language guide. One chapter per part of the language, from types and variables to naming conventions. Read it in order, or jump to the topic you need.
- Tools. The pepper command, pep-run, pep-fmt, pep-profile, pep-test and debugging.
- Standard library reference. Every type and function in the runtime, generated from the runtime’s own documentation comments. Start at the overview.
- Contributing. Building this documentation.
Every complete program in this book lives in the repository’s
docs/examples/ folder and is compiled and run by the test suite, so what
you read is what works.
Versions of this book
This documentation is versioned with the compiler. Use the version picker in
the top-right corner of each page to switch between releases; dev follows the main
branch. Read the version that matches the compiler you are using.
Getting started
This chapter installs Pepper on your machine, compiles and runs a first program, and sets up your editor.
Install Pepper
Each release has the Pepper toolchain prebuilt for Linux, macOS and Windows. One command installs it:
# Linux and macOS
curl -fsSL https://raw.githubusercontent.com/justinboswell/pepper/main/scripts/install.sh | sh
# Windows (PowerShell)
irm https://raw.githubusercontent.com/justinboswell/pepper/main/scripts/install.ps1 | iex
It installs into ~/.pepper (%LOCALAPPDATA%\pepper on Windows), adds its
bin folder to your PATH, and offers to install what linking a program
needs when it is missing: clang on Linux, Xcode’s command line tools on
macOS, LLVM and the Visual Studio Build Tools on Windows. Open a new terminal
afterwards. Installing Pepper covers the options, what
it installs, and uninstalling.
Building from source
To work on Pepper itself, or on a platform with no prebuilt binaries, build the toolchain from source: see Building Pepper from source.
Your first program
Pepper source files end in .pep. Create a file named hello.pep:
# Every program starts at main. Its return value is the process exit code.
main(): int {
print("Hello, world!");
return 0;
}
Every program starts at main, which here takes no arguments and returns an
int (it can also take the command-line arguments; see
Functions):
the process’s exit code, where 0 means success. print writes a value’s text to
standard output, followed by a new line. Comments start with # and run to
the end of the line.
Compile and run it
Pass the source file to pepper build. It compiles and links a native executable,
but does not run it, so run the result yourself:
pepper build hello.pep -o hello
./hello
Hello, world!
On Windows, name the output with .exe (-o hello.exe), since Windows will
not run a file without an extension, and run it as .\hello.exe. Without -o
the executable is written to the current directory, named after the source
file with no extension (hello for hello.pep).
Running a program
To build and run in one step, as python hello.py does, use pepper run.
It keeps the executable out of your folder, and runs it again at once when
nothing changed:
pepper run hello.pep
Arguments after the file go to the program’s main; see
Running a program: pep-run.
Where the compiler finds the standard library
Every program is compiled together with Pepper’s standard library, the
runtime/ folder of the repository. The compiler looks for it in this order:
- The directory named by the
PEPPER_RUNTIMEenvironment variable, if it is set. Nothing else is searched. - Starting from the directory you run it from:
runtime/,.pep/runtime/, then../runtime/,../../runtime/, and so on, up to five levels up. - Starting from the directory that holds the compiler executable:
runtime/beside it, then in each parent directory, up to five levels up.
So an installed compiler, in ~/.pepper/bin, finds ~/.pepper/runtime, and
one built in target/bin finds the runtime of the clone it was built
from, wherever you run it. To use a different runtime, set
PEPPER_RUNTIME or link the runtime into a project’s .pep folder:
mkdir .pep
ln -s /path/to/pepper/runtime .pep/runtime
If the compiler cannot find the runtime, it stops with an error:
error: cannot find the Pepper runtime: no runtime directory near /home/me/project or the compiler; set PEPPER_RUNTIME to its path
The compiler also writes intermediate files to .pep/build/ under the
directory you run it from, and keeps a build cache in .pep/cache/: each file
of the program (and the standard library) is compiled to its own object, and
on the next build a file whose sources did not change reuses its object
without being compiled or checked again, as long as what its imports declare
did not change either: editing a function’s body recompiles that file alone,
while changing a signature also recompiles the files that import it. A file
that compiled with warnings is checked again, so its warnings print on every
build. When nothing changed, nothing is checked or linked again. The cache also
keeps the standard library checked, so a program’s own files are the only ones
checked on each build. Deleting .pep/cache/ is always safe.
When something is wrong
The compiler reports errors the way clang does: the file, line and column, a message, and the source line with a caret under the exact token to fix. Store the message in a variable first, then misspell its name:
main(): int {
var greeting = "Hello, world!";
print(greting);
return 0;
}
The compiler reports:
main.pep:3:11: error: use of undeclared identifier 'greting'
3 | print(greting);
| ^~~~~~~
Errors are colored when the output is a terminal; set NO_COLOR to turn
color off. The chapter on compiler errors
covers them in more detail.
A failed assert or a panic at run time prints its message and stops the
program abnormally. For example, value() on a failed Result prints
Result.value on an Err value.
Useful compiler options
| Option | What it does |
|---|---|
-o <file> | Name the output executable (or library, with --lib). |
-O, -O<n> | Optimize: -O0 (the default) through -O3; -O alone is -O2. |
-g | Emit debug info for gdb, lldb, Visual Studio and WinDbg. See debugging. |
-I <dir> | Add a directory to search for imported modules. See modules and imports. |
-l <lib>, -L <dir> | Link a C library, and add a folder to find it in. See linking. |
-D <name>[=<value>] | Define a build option. |
--target <triple> | Compile for another platform; see Cross-compiling. |
--lib static, --lib shared | Build a library for C code to link. See calling Pepper from C. |
--help | List every option. |
The compiler: pep-build describes every option and environment variable the compiler reads.
A program can span several files: pass the entry file, and the compiler finds the files it imports on its own.
Editor support
The VS Code extension adds syntax highlighting and a language server, which reports compiler errors as you type and provides hover information, completion, go to definition, and an outline of each file.
The packaged extension is published as a .vsix file on the repository’s
releases page, under the
release whose tag starts with vsix-v. Download it and install it in VS Code
with Extensions: Install from VSIX… from the command palette. The
language server is bundled with the extension, so there is nothing else to
install.
A few things are useful once it is installed:
- Pepper: Restart Language Server restarts the server on demand.
- Where a value is converted by an
[Implicit]constructor, the editor shows the call it stands for as an inlay hint (Path(before"notes.txt"and)after), and hovering the value says so. Thepepper.inlayHints.implicitConversionssetting turns the hints off. - A literal passed by position shows the name of its parameter as an inlay
hint (
resize(width: 800, height: 600)); thepepper.inlayHints.parameterNamessetting turns these off. Inside a call’s parentheses, completion offers the parameter names to name (width =). - Hover and go to definition also work where the compiler has not checked
the code yet: in a generic function or method nothing calls, on a
matcharm’s case and the names it binds (Circle(r) => ...), and on the values an enum case declares. - The
pepper.lspPathsetting points the extension at a language server you built yourself (pep-build -O tools/pep-lsp/main.pep -o target/pep-lsp/pep-lspin a checkout of the repository; see The language server: pep-lsp). The server restarts whenever that file changes.
To install the extension from the branch you have checked out instead of the release, see The VS Code extension from a branch.
What to read next
- A tour of Pepper builds a small program step by step and shows the main features of the language along the way.
- The language guide covers each part of the language in depth.
- The standard library reference lists every built-in type and function.
A tour of Pepper
This chapter walks through the main features of Pepper by writing small programs, and ends with a word counter that reads a file and reports how often each word appears. It moves quickly and shows the common case of each feature; every section links to the chapter of the language guide that covers it fully.
Every program here is complete: save it to a file, compile it as shown in
Getting started, and run it. Each one checks its own
results with assert(condition, "message"), which stops the program with the
message if the condition is false, so a program that runs to the end and
returns 0 did what the comments say.
Variables and functions
# A function lists its parameters as `name: type` and gives its return
# type after the closing parenthesis.
square(x: int): int {
return x * x;
}
# A parameter may have a default value, used when the caller leaves it out.
greet(name: String = "world"): String {
return "Hello, " + name + "!";
}
# Adds up 1 + 2 + ... + n with a while loop.
sum_to(n: int): int {
var total = 0;
var i = 1;
while i <= n {
total += i;
i = i + 1;
}
return total;
}
describe(n: int): String {
if n > 100 {
return "big";
} else {
if n > 10 {
return "medium";
}
}
return "small";
}
main(): int {
var answer = 42; # the type, int, comes from the value
double ratio = 0.5; # or write the type yourself
var whole = (ratio * 10.0) as int;
assert(whole == 5, "'as' converts between number types");
assert(square(answer) == 1764, "square(42)");
assert(greet() == "Hello, world!", "the default name");
assert(greet("Pepper") == "Hello, Pepper!", "an explicit name");
assert(sum_to(4) == 10, "1 + 2 + 3 + 4");
assert(describe(answer) == "medium", "42 is medium");
assert(describe(7) == "small", "7 is small");
print(greet("Pepper"));
print($"{sum_to(100)}");
return 0;
}
A few things to notice:
vardeclares a variable whose type comes from its initial value:42is anint. You can also name the type yourself, as indouble ratio = 0.5;.asconverts between types, such as fromdoubletoint. See Types and variables.- A function is declared as
name(parameters): return type, and each parameter asname: type. A parameter can have a default value, asnamedoes ingreet. A function that returns nothing leaves out the return type. See Functions. ifandwhiledo not need parentheses around the condition, but the body always needs braces. Chain conditions withelse if. See Control flow.+=updates a variable in place, and+joins two strings.printwrites one value’s text on its own line. Interpolate to combine values:print($"total: {sum_to(100)}").
Structs
A struct groups related data with the methods that act on it:
struct Point {
var x: int = 0;
var y: int = 0;
# `this.x = 0` as a parameter stores the argument straight into the field.
construct(this.x = 0, this.y = 0) {}
length_squared(): int {
return this.x * this.x + this.y * this.y;
}
move_by(dx: int, dy: int) {
this.x = this.x + dx;
this.y = this.y + dy;
}
}
# An extension adds methods to a struct that is declared elsewhere.
extend Point {
swapped(): Point {
return Point(this.y, this.x);
}
}
main(): int {
var p = Point(3, 4);
assert(p.length_squared() == 25, "3*3 + 4*4");
var origin = Point();
assert(origin.x == 0 && origin.y == 0, "defaults from the constructor");
# Struct values are references: q and p name the same Point.
var q = p;
q.move_by(1, 1);
assert(p.x == 4 && p.y == 5, "p sees the move made through q");
var s = p.swapped();
assert(s.x == 5 && s.y == 4, "swapped is a new Point");
assert(p.x == 4, "p itself is unchanged");
return 0;
}
- Fields are declared like variables, with
var name: type, and may have default values. - The constructor is named
construct. A parameter written asthis.x = 0stores its argument straight into the fieldx, and= 0makes it optional, soPoint()andPoint(3, 4)both work. - Methods are functions declared inside the struct. Inside a method,
thisis the object the method was called on. extend Point { ... }adds methods to a struct from outside its declaration, even to a struct from the standard library.
Struct values are references. var q = p; does not copy the point: p and
q refer to the same object, so moving q moves p. Pepper frees an object
when the last reference to it goes away, so you never free memory yourself.
See Structs and
Memory and reference counting.
Collections
The standard library provides growable arrays, strings, and hash maps. You do not need to import them:
main(): int {
# Array<T> is a growable list. int[] is another way to write Array<int>.
var scores = [90, 72];
scores.append(85);
assert(scores.count == 3, "three scores");
assert(scores[0] == 90, "indexing starts at 0");
scores[1] = 75;
assert(scores.contains(85), "contains");
var total = 0;
for (var score in scores) {
total += score;
}
assert(total == 250, "90 + 75 + 85");
# sort orders the elements by a comparison function.
scores.sort((a, b) => a - b);
assert(scores[0] == 75 && scores[2] == 90, "sorted ascending");
int[] none = Array<int>();
assert(none.is_empty, "a new array is empty");
# String holds UTF-8 text.
var name = "Pepper";
var greeting = "Hello, " + name;
greeting += "!";
assert(greeting == "Hello, Pepper!", "+ and += join strings");
assert(greeting.length == 14, "length counts characters");
assert(greeting.starts_with("Hello"), "starts_with");
assert(greeting.substr(7, 6) == "Pepper", "substr(start, count)");
assert(greeting.find("Pepper") == 7, "find returns an offset, or -1");
assert(greeting[0] == "H", "s[i] is a one-character String");
# HashMap<K, V> maps keys to values.
var ages = ["Ada": 36];
ages.insert("Alan", 41);
assert(ages.count == 2, "two entries");
assert(ages["Ada"] == 36, "look up by key");
assert(ages.contains("Alan"), "contains");
assert(ages["Grace"] == 0, "a missing key reads as the default value");
assert(!ages.contains("Grace"), "reading does not insert");
ages.remove("Ada");
assert(ages.count == 1, "removed");
# HashSet<K> keeps each key once.
var seen = HashSet<String>();
seen.insert("apple");
seen.insert("apple");
assert(seen.count == 1, "duplicates are ignored");
return 0;
}
[90, 72]and["Ada": 36]are collection literals: a new array and a new map.Array<T>is a list that grows as youappend.int[]is a shorter way to write the typeArray<int>. Indexing is checked: reading past the end stops the program with an error rather than reading stray memory.sorttakes a function that compares two elements, such as the closure(a, b) => a - b.Stringholds UTF-8 text. Thelengthproperty counts characters (code points), ands[i]is the character at indexi, as a one-character string. Strings compare by content with==.HashMap<K, V>maps keys to values. Reading a key that is not there gives the default value ofV(0 for numbers) without adding it; usecontainsto tell the two apart.HashSet<K>keeps each key once.for (var x in collection)visits each element of an array, or each character of a string, in order, andfor (var i in 0..n)counts from 0 ton - 1. See Iteration.
Handling errors with Result
Pepper has no exceptions. A function that can fail returns a
Result: either a value, or an error that explains
what went wrong.
# Converts a string of decimal digits to an int, or explains why it cannot.
parse_number(text: String): Result<int> {
if text.is_empty {
return Result<int>.err(Error("expected a number, found nothing"));
}
var value = 0;
for (var c in text) {
var digit = "0123456789".find(c);
if digit < 0 {
return Result<int>.err(Error("'" + c + "' is not a digit"));
}
value = value * 10 + (digit as int);
}
return Result<int>.ok(value);
}
main(): int {
var good = parse_number("2026");
assert(good.is_ok(), "digits parse");
assert(good.value() == 2026, "value() returns the value");
var bad = parse_number("12x");
assert(bad.is_err(), "x is not a digit");
assert(bad.error().message == "'x' is not a digit", "the error explains why");
print(bad.error().message);
assert(parse_number("").ok_or(-1) == -1, "ok_or supplies a fallback");
return 0;
}
Result<int>holds either anintor anError. Build one withResult<int>.ok(value)orResult<int>.err(Error("message")).is_ok()andis_err()tell you which one you have.value()anderror()return what it holds;error().messageis the error’s text.value()returns the value, and stops the program if there is none. Use it only when an error would be a bug.ok_or(fallback)returns the value, orfallbackif there is none.
See Error handling with Result.
Putting it together: a word counter
The last program combines everything above. It writes a short text file,
reads it back, and counts how often each word appears. It is split into two
files: counter.pep defines a WordCounter struct, and main.pep uses it.
counter.pep:
# Counts how often each word occurs in some text.
export struct WordCounter {
var counts: HashMap<String, int>;
var order: String[]; # each distinct word, in the order first seen
var total: int;
construct() {
this.counts = HashMap<String, int>();
this.order = Array<String>();
this.total = 0;
}
add(word: String) {
if !word.is_empty {
if !this.counts.contains(word) {
this.order.append(word);
}
this.counts[word] = this.counts[word] + 1;
this.total = this.total + 1;
}
}
# Splits `text` into words at spaces and counts each one. `add` skips
# the empty pieces that runs of spaces leave.
add_text(text: String) {
for (var word in text.split(" ")) {
this.add(word);
}
}
count(word: String): int {
return this.counts[word];
}
}
main.pep:
import [File, Directory, Path] from io;
import "counter";
# Reads the file at `path` and counts its words.
count_file(path: Path): Result<WordCounter> {
var text = File.read_to_string(path);
if text.is_err() {
return Result<WordCounter>.err(Error(text.error().message));
}
var counter = WordCounter();
counter.add_text(text.value());
return Result<WordCounter>.ok(counter);
}
main(): int {
# Write a small file to read back.
var dir = Directory.temp().join("pepper-word-count");
Directory.create_all(dir);
var path = dir.join("poem.txt");
File.write_text(path, "the cat sat on the mat and the dog sat too");
var result = count_file(path);
assert(result.is_ok(), "the file we just wrote can be read");
var counter = result.value();
for (var word in counter.order) {
print($"{word}: {counter.count(word)}");
}
assert(counter.total == 11, "eleven words");
assert(counter.count("the") == 3, "'the' appears three times");
assert(counter.count("sat") == 2, "'sat' appears twice");
assert(counter.count("bird") == 0, "'bird' does not appear");
var missing = count_file(dir.join("missing.txt"));
assert(missing.is_err(), "a missing file is an error, not a crash");
print(missing.error().message);
Directory.remove_all(dir);
return 0;
}
Compile it by passing the entry file; the compiler finds counter.pep from
the import:
pepper build main.pep -o word-count
./word-count
It prints each word with its count, one word per line, and then the error message for the missing file (its exact text comes from the operating system):
the: 3
cat: 1
sat: 2
...
No such file or directory
What is new here:
- Imports.
import "counter";makes the declarations thatcounter.pepmarks withexportavailable inmain.pep. Anything not exported stays private to its file (or, in a module, to the module’s files).import [File, Directory, Path] from io;brings three names in from the standard library’siomodule, which, unlikeStringorArray, must be imported. See Modules and imports and Visibility. - Files.
Directory.temp()is the system’s temporary directory, andjoinadds a name to aPath; the name can be a plainString, which converts to aPathimplicitly.Filehas helpers that read or write a whole file at once:File.write_textandFile.read_to_string. - I/O errors are Results.
File.read_to_stringreturns aResultwhose error is anIoError, so reading a missing file is an ordinary error value, not a crash.count_fileturns it into its ownResult<WordCounter>. - Splitting strings.
add_textbreaks the text into words withsplit(" "), which returns anArray<String>of the pieces between the spaces (seeString).
Where to go next
You have now seen most of what you need to write everyday Pepper code. The language guide covers each topic in depth, including features this tour skipped: generics, function types and closures, traits and extensions, atomics, threads, and calling C.
Types and variables
This chapter covers the values every Pepper program is built from: numbers, booleans and strings, the variables that hold them, and the operators that combine them.
Built-in types
Integers
Pepper has signed and unsigned integers of 8, 16, 32 and 64 bits. Each width has
a short name (i32) and a descriptive alias (int). The two names are the same
type, so you can use whichever reads better and assign freely between them.
| Bits | Signed | Unsigned |
|---|---|---|
| 8 | i8, char | u8, byte |
| 16 | i16, short | u16, ushort |
| 32 | i32, int | u32, uint |
| 64 | i64, long | u64, ulong |
int is 32 bits and long is 64 bits on every platform. sizeof<T>() gives the
size of a type in bytes.
Integer literals are written in decimal (42) or hexadecimal (0xFF). A literal
on its own is an int when its value fits in an int, and a long otherwise, so
var big = 3000000000; is a long. It takes another type when you store it in a
variable of that type (u64 huge = 9000000000000000000;). A hexadecimal literal
is a 64-bit bit pattern: u64 all = 0xFFFFFFFFFFFFFFFF; sets every bit.
# Integer types: widths, signedness, wraparound and conversions.
main(): int {
# Each width has a short name and a descriptive alias.
i8 tiny = -100; # same type as char
u8 octet = 200; # same type as byte
i16 small = -30000; # same type as short
u16 port = 65000; # same type as ushort
i32 count = 2000000; # same type as int
u32 mask = 4000000000; # same type as uint
i64 big = 9000000000; # same type as long
u64 huge = 9000000000000000000; # same type as ulong
assert(sizeof<char>() == 1 && sizeof<short>() == 2, "8 and 16 bits");
assert(sizeof<int>() == 4 && sizeof<long>() == 8, "32 and 64 bits");
# An alias names the same type, so assigning between them is lossless.
short s = small;
assert(s == -30000, "short and i16 are one type");
# Storing a value that does not fit wraps around, as in C.
u16 wrap = 65535;
wrap = wrap + 1;
assert(wrap == 0, "u16 wraps from 65535 to 0");
i16 overflow = 32767;
overflow += 1;
assert(overflow == -32768, "i16 wraps from 32767 to -32768");
# Widening keeps the value: signed types sign-extend, unsigned zero-extend.
long widened = small;
assert(widened == -30000, "i16 widens to long");
assert((tiny as u8) == 156, "-100 as u8 is 156");
assert((octet as i8) == -56, "200 as i8 is -56");
# Narrowing with 'as' keeps the low bits.
assert((70000 as u16) == 4464, "70000 as u16 keeps the low 16 bits");
# Hexadecimal literals.
assert(0xFF == 255, "0xFF is 255");
assert(count + big > 0 && mask > 0 && huge > 0 && port > 0, "all in range");
return 0;
}
Storing a value that does not fit in the destination keeps its low bits, as in
C: a u16 holding 65535 becomes 0 when you add 1 and store the result, and
70000 as u16 is 4464. Arithmetic that overflows int or a wider type
panics instead (see Integer overflow).
A decimal literal larger than the largest long (9223372036854775807), or a
hexadecimal literal with more than 64 bits, is an error:
main.pep:2:13: error: integer literal is too large to be represented in a 64-bit integer
2 | var f = 99999999999999999999;
| ^~~~~~~~~~~~~~~~~~~~
Floating point
float is a 32-bit and double a 64-bit IEEE 754 number (f32 and f64 are
aliases). A literal with a decimal point or an exponent, such as 0.25 or 1e3,
is a double.
Comparisons follow IEEE 754, as in C: against NaN (0.0 / 0.0), ==, <,
<=, > and >= are false and != is true, so x != x is true only for a
NaN. As a condition, a float is true unless it equals zero, NaN included.
Reading numbers from text
Every number type has a static parse that reads one from a String:
int.parse("42") gives an int? (see optionals), empty when
the text is not a number of that type: not digits, or too big or too small for
it. The integer types take a base from 2 to 36 as a second argument ("ff" in
base 16 is 255), and float and double read decimal notation ("2.5",
".5", "1e-3"). Neither accepts spaces, a 0x prefix, inf or nan. Each
spelling of a type is the same type, so i32.parse is int.parse:
# Reading numbers from text with each number type's static parse.
main(): int {
# An integer type takes an optional base from 2 to 36.
assert(int.parse("-42").value() == -42, "decimal");
assert(byte.parse("ff", 16).value() == 255, "hexadecimal");
# The result is empty when the text is not a number of that type.
assert(int.parse("4 2").is_empty(), "not an integer");
assert(byte.parse("256").is_empty(), "too big for a byte");
# float and double read decimal notation.
var ratio = double.parse("2.5e-1");
if (ratio.is_value()) {
assert(ratio.value() == 0.25, "an exponent");
}
return 0;
}
Booleans
bool holds true or false. Comparisons and the logical operators produce a
bool. In arithmetic a bool counts as 0 or 1, and its text is true or
false ($"{flag}").
Characters
A character literal is one ASCII character between single quotes: 'a', '"',
'#'. Its value is the character’s code, and its type is byte (u8), the type
of one byte of a UTF-8 string. Like any byte it widens to the other integer
types, so it compares with, and stores into, an int or a char as well:
# Character literals are bytes.
is_lower(c: int): bool {
return c >= 'a' and c <= 'z';
}
main(): int {
var letter = 'a'; # a byte holding 97
assert(letter == 97, "a character literal is its ASCII code");
assert('"' == 34 and '\n' == 10 and '\'' == 39, "escapes work as in strings");
# Compare the bytes of a String with character literals.
var text = "x = 1";
var bytes = text.c_str();
assert(bytes[0] == 'x' and bytes[2] == '=', "compare raw bytes");
assert(is_lower(bytes[0]), "a byte widens to int");
# Arithmetic works as on any integer.
int digit = bytes[4] - '0';
assert(digit == 1, "the digit's value");
print($"{letter}"); # 97
return 0;
}
The escapes are the same as a string’s (see the table under
Strings), plus \xHH for any byte, '\xFF' included. A
character literal holds exactly one ASCII character; for anything else use a
string:
main.pep:2:13: error: character literal may only contain one character; use a string for more
2 | var x = 'ab';
| ^~~~
main.pep:2:14: error: character literal must be a single ASCII character; use a string for other characters
2 | var x = 'é';
| ^
char is an 8-bit signed integer, the same type as i8, used mostly for C
strings (__ptr<char>). The ASCII characters have the same value as a char
and as a byte; '\xFF' is 255 as a byte but -1 read through a char.
Text is a String, and a single character of it is a one-code-point String.
byte and char classify ASCII characters as C’s ctype.h does in the “C”
locale: is_digit, is_hex_digit, is_alphabetic, is_alphanumeric,
is_upper, is_lower, is_whitespace, is_blank, is_punctuation,
is_graphic, is_printable, is_control and is_ascii, plus to_upper,
to_lower and digit_value(base). The names are spelled out (is_punctuation
for C’s ispunct). A byte above 127, which is a negative char, is never a
letter, digit, space or punctuation. Unicode classification is not covered.
# Classify and convert ASCII characters, as C's ctype.h does.
main(): int {
assert('7'.is_digit() and 'f'.is_hex_digit(), "digits");
assert('q'.is_alphabetic() and 'Q'.is_upper() and 'q'.is_lower(), "letters");
assert('\t'.is_whitespace() and '\t'.is_blank(), "whitespace");
assert(';'.is_punctuation() and !'_'.is_alphanumeric(), "punctuation");
assert('\n'.is_control() and !'\n'.is_printable(), "control");
assert('a'.to_upper() == 'A' and '?'.to_upper() == '?', "case");
# The digit's value in a base, or none when it is not a digit there.
assert('F'.digit_value(16).value() == 15, "hexadecimal");
assert('9'.digit_value(8).is_empty(), "not an octal digit");
# Count the words and digits of a String, a byte at a time.
var text = "Call 555-0100 at 9 a.m.";
var words = 0;
var digits = 0;
var in_word = false;
for (var b in text.bytes()) {
if (b.is_digit()) {
digits = digits + 1;
}
if (b.is_alphanumeric() and !in_word) {
words = words + 1;
}
in_word = b.is_alphanumeric();
}
# A char answers the same, and bytes above 127 (negative as a char)
# are never letters.
char e_acute = -61; # the first byte of "é" in UTF-8
assert(!e_acute.is_alphabetic() and !e_acute.is_ascii(), "not ASCII");
print($"{words} words, {digits} digits"); # 7 words, 8 digits
return 0;
}
Strings
String is a UTF-8 string. A string literal such as "hello" is a String
value wherever a String is expected, and wherever nothing asks for another
type (var s = "hello"; declares a String). Where it is stored or passed as
a __ptr<char>, it is the literal’s NUL-terminated bytes (see
Passing strings), and where a
StringConstant is expected, it is one. + joins two strings into a new one, == and != compare contents,
the length property counts code points, size counts bytes, and s[i] gives the code
point at index i as a one-code-point String.
# String literals are String values.
main(): int {
var language = "Pepper";
assert(language.length == 6, "6 code points");
# + makes a new String; == compares contents.
var message = "Hello, " + language;
assert(message == "Hello, Pepper", "concatenation");
assert(message != language, "different contents");
# Strings are UTF-8. `length` counts code points, `size` counts bytes.
var word = "héllo";
assert(word.length == 5, "5 code points");
assert(word.size == 6, "6 bytes: 'é' takes 2");
assert(word[1] == "é", "indexing yields a one-code-point String");
# A String is a reference: both names see an append.
var alias = message;
message.append("!");
assert(alias == "Hello, Pepper!", "one object, two references");
print(message);
return 0;
}
A String is a reference type: var alias = message; makes a second reference
to the same object rather than a copy, so a change made with append shows
through both names. Memory and reference counting explains how
references work, and the String reference lists every
method.
Every place a literal is stored (a variable, a field, an array element, a
function’s result) gets a new String, so changing it never changes the
literal: after var s = "a"; s.append("b"); the next "a" is still "a".
Where a literal is only read, passed to a parameter that the function
neither keeps nor changes (builder.append("x"), name.contains("x")), every
evaluation shares one constant object and allocates nothing.
String constants
A literal’s own type is StringConstant: a pointer
to the literal’s bytes, which never change and are never freed, with their
size and length. It is a [Value] type, so copying one
steps no reference count, any thread may read it, and a literal stored as one
allocates nothing. Its bytes always end in a NUL byte, so c_str() passes it
to C as it is (a String’s c_str() does too, but a String built with no
data has none).
A StringConstant has String’s read API (length, size, find,
split, trim, ==, <, the indexer, for loops and so on); the methods
that make new text return a String. It converts to a String wherever a
String is expected, as if String had an [Implicit]
constructor taking it: the conversion (and
String(text)) copies the bytes, so an API that takes a String takes a
StringConstant too. Where C expects a char*, it passes its own bytes, as
c_str() does. Among overloads, a literal picks a
StringConstant parameter over a String one, and a StringConstant value
picks a String parameter last, as an implicit conversion.
# A string literal's own type is StringConstant: its constant bytes, with no
# allocation and no reference count.
extern ulong strlen(char* text);
# A `const` initialized from a literal is a StringConstant.
const GREETING = "hello";
shout(text: String): String {
return text.upper() + "!";
}
main(): int {
StringConstant name = "pepper";
assert(name.length == 6, "the read API of String");
assert(name.starts_with("pep") && name.find("p", 1) == 2, "find counts bytes");
# Always NUL-terminated: c_str() goes to C with no copy.
assert(strlen(GREETING.c_str()) == 5, "C reads the literal's bytes");
# A StringConstant converts to a String wherever one is expected.
String copy = name;
copy.append("!");
assert(copy == "pepper!" && name == "pepper", "the copy has its own bytes");
print(shout(GREETING));
# Compared with a String either way round, with no copy.
String typed = "pepper";
assert(name == typed && typed == name && GREETING < typed, "mixed comparisons");
# Copying a StringConstant copies a pointer.
var also = name;
assert(also == name, "same text");
return 0;
}
- Where a literal is a
StringConstant: a variable, field, parameter or return value of typeStringConstant, aHashMap<StringConstant, V>key, and aconstwith no written type (const GREETING = "hello";), so a module-scope text constant allocates nothing and any thread may read it. Everywhere else,var s = "x"included, a literal is aString, as before. - Converting chains once: a literal reaches a type that converts from
String(io.Path,Error) directly, but aStringConstantvariable does not, since conversions never chain. WriteString(text)or pass a literal. - Comparing:
==,!=,<,<=,>and>=compare aStringConstantwith anotherStringConstant(a literal included) or with aString, either way round, and copy nothing. - A literal method call (
"a,b".split(",")) calls theStringmethod.
to_string turns any integer into its decimal text. For parsing and
generating text, byte_at and substr_bytes work in bytes (the offsets
find returns), compare and <, <=, >, >= order strings byte by byte
(code-point order for UTF-8), lines() splits on "\n" or "\r\n",
trim(), trim_start() and trim_end() drop whitespace, lower() and
upper() change the case of ASCII letters, repeat(n) joins n copies, and
bytes() copies the UTF-8 bytes into an array. A
StringBuilder builds a long String from many pieces without recopying the
text built so far on every append:
# Numbers to text, byte-level access, ordering, and building text in pieces.
main(): int {
# to_string turns any integer into its decimal text.
var line = 3;
var where = to_string(line) + ":" + to_string(-7);
assert(where == "3:-7", "to_string");
# byte_at and substr_bytes count bytes, so they work with find's offsets.
var setting = "indent=4";
var eq = setting.find("=") as ulong;
assert(setting.byte_at(eq) == '=', "byte_at compares with a character literal");
assert(setting.substr_bytes(0, eq) == "indent", "the bytes before '='");
# compare, <, <=, > and >= order strings byte by byte.
assert("apple" < "banana", "ordering");
assert("b".compare("a") > 0, "compare is negative, 0 or positive");
# lines() splits on "\n" or "\r\n"; trim_end() drops trailing whitespace.
var lines = "one \r\ntwo\n".lines();
assert(lines.count == 2, "no empty line after the final newline");
assert(lines[0].trim_end() == "one", "trim_end");
# trim() drops whitespace at both ends; lower() and upper() change ASCII case.
var field = " Content-Type ".trim();
assert(field.lower() == "content-type", "lower");
assert(field.upper() == "CONTENT-TYPE", "upper");
# repeat joins copies; StringBuilder appends without recopying the text.
var out = StringBuilder();
for (var text in lines) {
out.append(" ".repeat(4)).append(text.trim_end()).append("\n");
}
assert(out.to_string() == " one\n two\n", "StringBuilder");
return 0;
}
To search and take text apart, find(needle, from) starts at a byte offset
and rfind finds the last match, split(separator) cuts at every separator
and split_once at the first (giving an optional pair), separator.join(parts)
puts the parts back together, and replace(from, to) swaps every match
(to read a number from text, see
Reading numbers from text):
# Searching, splitting, joining and replacing text.
main(): int {
# find takes a byte offset to start from; rfind finds the last match.
var path = "src/lib/main.pep";
var first = path.find("/");
assert(path.find("/", (first as ulong) + 1) == 7, "the second slash");
assert(path.rfind("/") == 7, "the last slash");
# split cuts at every separator; split_once at the first, if any.
var fields = "name,,age".split(",");
assert(fields.count == 3 && fields[1] == "", "an empty field");
var header = "Content-Type: text/plain".split_once(": ");
if (header.is_value()) {
var (name, value) = header.value();
assert(name == "Content-Type" && value == "text/plain", "both halves");
}
# join puts the separator between the parts; replace swaps every match.
assert(" | ".join(fields) == "name | | age", "joined");
assert("a\r\nb\r\n".replace("\r\n", "\n") == "a\nb\n", "line endings");
return 0;
}
An interpolated string puts values’ text into a string literal:
$"line {line + 1}: {message}". String interpolation covers
which values it accepts.
A backslash in a string literal starts an escape sequence, as in C and Rust:
| Escape | Character |
|---|---|
\n | newline |
\t | tab |
\r | carriage return |
\0 | the zero byte |
\\ | a backslash |
\" | a double quote |
\' | a single quote |
\xHH | the ASCII character with hexadecimal code HH, at most \x7F (any byte in a character literal) |
\u{H...} | the Unicode code point with hexadecimal code H... (1 to 6 digits) |
print("name:\t\"Pepper\""); # name: "Pepper"
print("caf\u{e9}"); # café
Any other character after a backslash is an error:
main.pep:2:13: error: unknown escape sequence '\q'
2 | print("a\qb");
| ^~
Other built-in types
voidmeans “no value”. It is the return type of a function that returns nothing.Array<T>is a growable array ofT;int[]is shorthand forArray<int>, andint[][]forArray<Array<int>>. SeeArray.Optional<T>is a value that may be missing;int?is shorthand forOptional<int>. See Optional values.HashMap<K, V>andHashSet<K>are hash tables. See HashMap and HashSet.Result<TValue, TError>andErrordescribe operations that can fail. See Error handling with Result.async.Atomic<T>is a value that threads can update safely. See Atomics and Threads and shared data.
Raw pointers and __null
For talking to C, Pepper has raw pointers, written __ptr<T> (for example
__ptr<char> or __ptr<void>), and the null pointer literal __null:
var p: __ptr<void> = __null;
assert(p == __null, "p is null");
Most programs never need them. Calling C covers pointers in full.
Naming conventions
Primitive types and keywords are lowercase: int, float, bool, var,
while. Types, including the ones the runtime provides, are PascalCase:
String, Array<T>, HashMap<K, V>, Result<TValue, TError>. Functions,
variables and fields are snake_case, and constants are SCREAMING_CASE.
Naming conventions has the full rules.
Identifiers are made of ASCII letters, digits and underscores, and cannot start
with a digit. Avoid names that start with two underscores: the compiler and
runtime use them for their own names, such as __ptr and __null.
Variables
Declare a variable with var and let Pepper infer its type from the initial
value, or write the type yourself. Prefer var whenever the type is obvious from
the value.
import async;
# Declaring variables: inferred, explicitly typed, constant, and module-level.
# Module-level variables (globals) live for the whole program. Any thread
# can reach them, so they are const and of a thread-safe type: a number, or
# an Atomic for state that changes.
const MAX_REQUESTS = 10;
const request_count = async.Atomic<int>(0);
const total_bytes = async.Atomic<long>(0);
record(bytes: int) {
request_count.fetch_add(1);
total_bytes.fetch_add(bytes);
}
main(): int {
# 'var' infers the type from the initializer.
var answer = 42; # int: integer literals are int
var ratio = 0.25; # double: floating-point literals are double
var ready = true; # bool
var name = "Pepper"; # String
# Write the type first, C style...
int width = 80;
float scale = 1.5;
# ...or after the name with a colon.
var height: int = 24;
var offset: long = 5000000000;
# A variable declared without a value starts at zero.
var hits: int;
assert(hits == 0, "numbers start at zero");
# 'const' declares a name that should not be reassigned.
const LIMIT = 3;
record(100);
record(250);
assert(request_count.load() == 2, "two requests recorded");
assert(total_bytes.load() == 350, "350 bytes in total");
assert(request_count.load() < MAX_REQUESTS, "globals are visible in every function");
assert(answer + width + height == 146, "sum of ints");
assert(ratio * 4 == 1.0 && scale == 1.5, "floating point");
assert(ready && name == "Pepper", "bool and String");
assert(offset > 4000000000 && LIMIT == 3, "long and const");
return 0;
}
The three forms are:
var name = value;infers the type from the value.Type name = value;is the C style, with the type first.var name: Type = value;puts the type after the name.
A variable declared without a value starts out as zero: 0 for numbers,
false for bool, and null for a struct type such as String. Give struct
variables a value before you use them.
When var infers the type of an arithmetic expression, it takes the type the
arithmetic produces under C’s rules (see Arithmetic follows C):
var m = 1 + 0.5; is a double holding 1.5, and with u8 small = 250;,
var s = small + 10; is an int holding 260.
Constants
const declares a name that you do not intend to change:
const LIMIT = 3;
const RATIO: double = 0.5;
Assigning to a const, with =, a compound assignment, ++ or --, is an
error:
main.pep:5:5: error: cannot assign to const variable 'LIMIT'
5 | LIMIT = 4;
| ^~~~~
main.pep:1:7: note: 'LIMIT' declared const here
1 | const LIMIT = 3;
| ^~~~~
Module-level variables
A variable declared outside any function is a module-level (global) variable. It
exists for the whole run of the program and every function in the file can read
it. Its initializer can be any expression, including a function call, and runs
before main. Like other top-level declarations, a global is private to its
file (or module) unless you mark it export (see Visibility).
Any function can run on any thread, so every thread can reach a global. It
must therefore be declared const and have a thread-safe type, such as a
number, an Atomic<T>, a ThreadLocal<T>, or a [Shared] struct (see
Module-scope variables). A global that
changes is an Atomic<T>, or a [ThreadLocal] var, which every thread has
its own copy of (see Thread-local variables):
const MAX_RETRIES = 3;
const requests = async.Atomic<long>(0); # changed with requests.fetch_add(1)
[ThreadLocal]
var depth: int = 0; # one per thread
Casting with as
value as Type converts a value to another type explicitly, like static_cast
in C++:
var ratio = 7.9;
var whole = ratio as int; # 7
var wide = whole as long;
- Floating point to integer truncates toward zero:
-7.9 as intis-7. - Integer to a narrower integer keeps the low bits:
70000 as u16is 4464. - Integer to a wider integer sign-extends a signed source and zero-extends an
unsigned one:
(-1 as i16) as longis -1, and(65535 as u16) as longis 65535. booland enums convert like integers:true as intis 1, and an enum converts to the integer it is stored as. An integer does not convert to an enum; name a case instead.- A struct converts to a trait it conforms to:
dog as Named. The other way, from a trait value, may fail:shape as? Squaregives aSquare?(see Converting a trait value back). - A pointer converts to
__ptr<void>and back:p as __ptr<void>, thenv as __ptr<int>. - A value converts to its own type.
Anything else is a compile error at the as, since it would not be a value
conversion but a reinterpretation of memory:
main.pep:3:15: error: cannot cast '__ptr<int>' to 'ulong' with 'as'; use 'unsafe_cast<ulong>(...)' to reinterpret
3 | var a = p as ulong;
| ^~
Casting between unrelated struct types, or between a struct and a number or
pointer, is an error with no alternative (cannot cast 'P' to 'Q').
as binds more tightly than the arithmetic operators, so x * 10 as int means
x * (10 as int). Put parentheses around the expression you want to convert:
(x * 10) as int. C-style casts such as (int)x do not exist.
Many conversions also happen without as: assigning or passing a number to a
variable or parameter of another numeric type converts it, as in C, including
narrowing conversions (int n = 2.7; stores 2).
Reinterpreting with unsafe_cast
unsafe_cast<Type>(value) reinterprets a value’s bits as another type, like
reinterpret_cast in C++. It is for C interop and low-level code, and nothing
checks that the result makes sense:
- A pointer to a pointer of another type:
unsafe_cast<__ptr<u8>>(text.c_str()). - A pointer to an integer, and an integer to a pointer:
unsafe_cast<ulong>(p),unsafe_cast<__ptr<int>>(address). - A number to another number of the same width, keeping its bits:
unsafe_cast<uint>(1.0 as float)is0x3F800000, andunsafe_cast<double>(bits)turns aulongback into adouble.
# `as` converts values; `unsafe_cast<T>(value)` reinterprets bits.
extern void* malloc(ulong size);
extern void free(void* p);
main(): int {
# A float's bits, and back: the same 32 bits seen as a uint.
var one = 1.0 as float;
var bits = unsafe_cast<uint>(one);
assert(bits == 0x3F800000, "1.0 is 0x3F800000");
assert(unsafe_cast<float>(bits) == one, "the bits convert back");
# A double's sign bit is its top bit.
var negative = unsafe_cast<ulong>(-2.0);
assert((negative >> 63) == 1, "the sign bit is set");
# `as` converts a pointer to and from `__ptr<void>`.
var memory = malloc(8);
var words = memory as __ptr<uint>;
words[0] = 0x01020304;
words[1] = 0;
# Reading the same memory as bytes reinterprets it.
var bytes = unsafe_cast<__ptr<u8>>(words);
assert(bytes[0] == 4 || bytes[0] == 1, "the first byte is the low or the high one");
# An address is an integer, and an integer is an address.
var address = unsafe_cast<ulong>(words);
assert(unsafe_cast<__ptr<uint>>(address + 4)[0] == 0, "the address four bytes on is the second word");
free(memory as __ptr<void>);
return 0;
}
It never converts a struct, trait or function value, so it cannot get around
reference counting. A conversion as can make is not a reinterpretation, so
unsafe_cast<long>(n) on an int is an error that says to use as:
main.pep:3:13: error: 'unsafe_cast' cannot convert 'int' to 'long': the sizes differ; use 'as' to convert the value
3 | var w = unsafe_cast<long>(n);
| ^~~~~~~~~~~
Arithmetic follows C
Arithmetic on numbers behaves exactly as it does in C (the “usual arithmetic conversions”), so code ported from C gives the same results:
- Integer promotion. Types narrower than
int(char,u8,short,u16, andbool) are converted tointbefore any arithmetic. The sum of twochars is anint, so100 + 100gives 200 rather than wrapping at 8 bits. - Mixed widths use the wider type:
int + longis along. - Signed and unsigned. When both operands have the same width and one is
unsigned, the operation is unsigned: comparing
int -1withuint 1treats -1 as 4294967295, so-1 < 1is false for those types. - Integer and floating point gives floating point:
3 * 0.5is 1.5. - Division of integers truncates toward zero (
-7 / 2is -3), and%takes the sign of the left operand (-7 % 3is -1). - Conditionals.
c ? a : bwith numeric arms of different types has their common type by the same rules (c ? 0 : s.lengthis aulong), and so does amatchexpression over its arms. The value converts from that type wherever it goes, and avarit initializes has that type (var n = c ? 0 : s.lengthis aulong, not anint). - Shifts take the type of the (promoted) left operand.
>>keeps the sign for signed types and fills with zeros for unsigned types. - Overflow is where Pepper leaves C: it panics, for signed and unsigned types alike (see Integer overflow).
# Arithmetic follows C's rules for promotion and conversion.
main(): int {
# Integer division truncates toward zero; % takes the sign of the left side.
assert(7 / 2 == 3, "7 / 2 is 3");
assert(-7 / 2 == -3, "-7 / 2 is -3");
assert(-7 % 3 == -1, "-7 % 3 is -1");
# Types narrower than int are promoted to int before arithmetic,
# so the sum of two chars does not wrap at 8 bits...
char a = 100;
char b = 100;
int sum = a + b;
assert(sum == 200, "char + char is computed as int");
# ...but storing the result back into a char does.
char c = a + b;
assert(c == -56, "200 does not fit in a char");
# Mixing signed and unsigned of the same width is unsigned, as in C.
int minus_one = -1;
uint one = 1;
assert(!(minus_one < one), "-1 converts to 4294967295 when compared with a uint");
uint zero = 0;
assert(zero.wrapping_sub(1) == 4294967295, "zero - 1 would panic; wrapping_sub wraps");
# Mixing an integer with a floating-point value gives floating point.
int whole = 3;
double half = 0.5;
double product = whole * half;
assert(product == 1.5, "3 * 0.5 is 1.5");
# 'as' converts explicitly. Floating point to integer truncates toward zero.
assert((7.9 as int) == 7, "7.9 as int is 7");
assert((-7.9 as int) == -7, "-7.9 as int is -7");
assert(((whole as double) / 2) == 1.5, "divide as double");
# Assignment converts implicitly, including from double to int.
int truncated = 2.7;
assert(truncated == 2, "2.7 stored in an int is 2");
# Shifts: >> is arithmetic for signed types and logical for unsigned types.
assert(-8 >> 1 == -4, "signed shift keeps the sign");
uint bits = 0x80000000;
assert(bits >> 31 == 1, "unsigned shift fills with zeros");
return 0;
}
Integer overflow
Integer arithmetic whose result does not fit its type stops the program, as in Swift (and Rust’s debug builds), instead of wrapping around silently:
main.pep:3:16: panic: integer overflow in 'int' addition
The message names the operator’s location and the type the operation ran at. These operations check:
+,-,*and**, unary-,++and--, and the compound assignments (+=,-=,*=). Negating the most negative value of a signed type overflows, and so does negating any nonzero unsigned value or subtracting past 0 (uint 0 - 1)./and%by zero (division by zero), and the most negative value of a signed type divided by -1 (MIN / -1andMIN % -1), whose result does not fit.<<and>>by a count of at least the type’s width, or a negative one:shift count out of range for 'int': must be from 0 to 31.
Arithmetic on types narrower than int runs at int (see above), so it
overflows only past int’s range; storing the result back in the narrow type
keeps its low bits, like any narrowing conversion. x++ on a u8 holding 255
leaves 0, as x += 1 does. Casts with as never check: 70000 as u16 is
4464.
When both operands are constants, an overflow is a compile error at the operator, and so is a division by a constant 0:
main.pep:2:28: error: integer overflow in 'int' multiplication
2 | var seconds = 100000 * 100000;
| ^
Wrapping on purpose
Hashes, checksums and random number generators rely on wrapping. Each integer type has methods that say what to do on overflow instead of panicking:
| Method | On overflow |
|---|---|
wrapping_add(x), wrapping_sub(x), wrapping_mul(x) | Wraps around: the result modulo 2^bits. |
checked_add(x), checked_sub(x), checked_mul(x) | Returns Empty (the result is a T?). |
saturating_add(x), saturating_sub(x), saturating_mul(x) | Clamps to the type’s highest or lowest value. |
overflowing_add(x), overflowing_sub(x), overflowing_mul(x) | Returns the wrapped result and true as a (T, bool). |
# Arithmetic that overflows panics; these methods say what to do instead.
main(): int {
int big = 2147483647;
# big + 1 would stop the program: "integer overflow in 'int' addition".
# wrapping_ wraps around, as C's unsigned arithmetic does.
assert(big.wrapping_add(1) == -2147483648, "wraps to the lowest int");
# checked_ returns none on overflow.
assert(big.checked_add(1).is_empty(), "no int is big + 1");
assert(big.checked_sub(1).value() == 2147483646, "big - 1 fits");
# saturating_ clamps to the type's range.
assert(big.saturating_mul(2) == 2147483647, "clamped to the highest int");
byte level = 10;
assert(level.saturating_sub(20) == 0, "a byte stops at 0");
# overflowing_ returns the wrapped result and whether it overflowed.
var (product, overflowed) = big.overflowing_mul(2);
assert(product == -2 && overflowed, "wrapped, and overflowed");
# A hash multiplies with wrapping on purpose (FNV-1a).
ulong hash = 0xcbf29ce484222325;
var text = "pepper";
for (var i in 0..text.size) {
hash = (hash ^ (text.byte_at(i) as ulong)).wrapping_mul(0x100000001b3);
}
assert(hash == 0xac11131d51a77079, "the FNV-1a hash of 'pepper'");
return 0;
}
The methods work at their type’s own width: byte 200.checked_add(100) is
Empty, while 200 + 100 on two bytes is 300 at int.
Removing the checks
The checks stay on at every optimization level. A build that has measured
their cost and accepts the risk can remove kinds of check with
--no-runtime-checks, a comma-separated list:
| Name | Removes | Then |
|---|---|---|
overflow | The overflow checks of +, -, *, **, unary -, ++, -- | Arithmetic wraps around (never undefined). |
divide | Division and remainder by zero, and MIN / -1 | The result is undefined, as in C. |
shift | The shift count check | The count is masked to the type’s width (x << 33 on an int shifts by 1). |
bounds | Array and String index checks (a[i], byte_at) | An index out of range reads or writes arbitrary memory. |
null | Calls through null function and trait values, and match on a null reference | The program crashes or misbehaves. |
all | Every check above |
pepper build -O main.pep --no-runtime-checks=overflow,shift
Removing bounds or null gives up Pepper’s memory safety: an index out of
range or a null reference then reads or writes arbitrary memory instead of
panicking. The flag does nothing at -O0, the default, where every check stays
on and the compiler warns
warning: '--no-runtime-checks' has no effect at -O0. It applies from -O1
up, -g included. The setting is part of the build cache’s key, like the
optimization level.
To remove checks from one function instead of a whole build, mark it
[Unsafe("...")] with the same names, comma-separated in one string:
[Unsafe("overflow")], [Unsafe("overflow,bounds")] or [Unsafe("all")].
It removes them from that function’s body only, at every optimization level
(-O0 included), so the function behaves the same in every build. Closures
written in it lose the same checks. Functions it calls keep theirs, with one
exception: bounds also removes the index checks that Array and String
make for it, since those run inside their indexers. [Unsafe] goes on a free
function, a method or a constructor. An unknown name is an error at the
string.
# `[Unsafe("...")]` removes runtime checks from one function's body.
# A hash that wraps on purpose: without the overflow check, `*` and `+`
# wrap around instead of panicking.
[Unsafe("overflow")]
hash(values: Array<int>): uint {
uint result = 2166136261;
for (var value in values) {
result = (result ^ (value as uint)) * 16777619;
}
return result;
}
# A loop that already checked its range: the index checks of `a[i]`, made by
# `Array`'s indexer for this caller, are removed too.
[Unsafe("bounds")]
sum(values: Array<int>): long {
long total = 0;
ulong i = 0;
while (i < values.count) {
total = total + values[i];
i = i + 1;
}
return total;
}
main(): int {
var values = Array<int>();
for (var i in 0..100) {
values.append(i);
}
assert(hash(values) != 0, "a hash");
assert(sum(values) == 4950, "0 + 1 + ... + 99");
# Outside those functions every check stays: 2147483647 + 1 here would
# still panic.
return 0;
}
Bit operations and math functions
Each integer type has methods that compile to a single instruction on most CPUs, through LLVM’s intrinsics, for code that would otherwise loop over bits:
| Method | Returns |
|---|---|
leading_zeros(), trailing_zeros() | The zero bits above the highest set bit, or below the lowest; the type’s width for zero. |
count_ones(), count_zeros() | How many bits are set, or clear. |
swap_bytes() | The value with its bytes reversed, to convert between little and big endian. |
reverse_bits() | The value with its bits reversed. |
rotate_left(n), rotate_right(n) | The bits rotated by n, modulo the width. |
min(x), max(x) | The smaller or the larger value. |
abs() | The value without its sign (signed types). Panics for the lowest value, whose negation does not fit. |
Like the overflow methods, they work at their type’s own width:
byte 1.leading_zeros() is 7.
# Bit operations on integers: counting, reversing and rotating bits.
main(): int {
uint mask = 0x00F0;
assert(mask.trailing_zeros() == 4, "the lowest set bit is bit 4");
assert(mask.leading_zeros() == 24, "the highest set bit is bit 7");
assert(mask.count_ones() == 4, "four bits are set");
# Zero has no set bit: its counts are the type's width.
assert((0 as ulong).trailing_zeros() == 64, "no set bit");
# The methods work at their type's own width.
byte low = 1;
assert(low.leading_zeros() == 7, "a byte has 8 bits");
assert(low.rotate_right(1) == 0x80, "the bottom bit comes back at the top");
# Byte order: swap_bytes converts between little and big endian.
assert((0x12345678 as uint).swap_bytes() == 0x78563412, "bytes reversed");
assert((1 as uint).reverse_bits() == 0x80000000, "bits reversed");
# min, max and abs.
assert((-7).abs() == 7, "without its sign");
assert((3).min(5) == 3 && (3).max(5) == 5, "smaller and larger");
return 0;
}
float and double have square_root(), floor(), ceiling(),
truncate(), round() (halfway cases away from zero), abs(),
copy_sign(sign), multiply_add(factor, addend) (this * factor + addend
rounded once) and min(x) and max(x) (which ignore a NaN argument):
# Floating-point functions: square root, rounding, sign and fused
# multiply-add.
main(): int {
var side = (2.0).square_root();
assert(side * side > 1.999 && side * side < 2.001, "close to 2");
assert((-1.5).floor() == -2.0, "toward negative infinity");
assert((-1.5).ceiling() == -1.0, "toward positive infinity");
assert((-1.5).truncate() == -1.0, "toward zero");
assert((2.5).round() == 3.0, "halfway cases away from zero");
assert((-4.0).abs() == 4.0, "without its sign");
assert((4.0).copy_sign(-1.0) == -4.0, "the sign of the argument");
assert((2.0).multiply_add(3.0, 1.0) == 7.0, "2 * 3 + 1, rounded once");
assert((1.5).min(-2.0) == -2.0 && (1.5).max(-2.0) == 1.5, "smaller and larger");
return 0;
}
They also have the functions of C’s <math.h>, with their names spelled
out. Angles are in radians. LLVM’s intrinsics compute the ones it has, and
the C math library the rest:
| Methods | C’s names |
|---|---|
sine(), cosine(), tangent() | sin, cos, tan |
arc_sine(), arc_cosine(), arc_tangent(), y.arc_tangent2(x) | asin, acos, atan, atan2(y, x) |
hyperbolic_sine(), hyperbolic_cosine(), hyperbolic_tangent() | sinh, cosh, tanh |
arc_hyperbolic_sine(), arc_hyperbolic_cosine(), arc_hyperbolic_tangent() | asinh, acosh, atanh |
exponential(), exponential2(), exponential_minus_one() | exp, exp2, expm1 |
logarithm(), logarithm2(), logarithm10(), logarithm_one_plus() | log, log2, log10, log1p |
cube_root(), hypotenuse(other) | cbrt, hypot |
remainder(divisor) | remainder (the quotient rounded to nearest; % is fmod, rounded toward zero) |
is_nan(), is_infinite(), is_finite(), sign_bit() | isnan, isinf, isfinite, signbit |
Powers are the ** operator: x ** 0.5, or x ** 3 with an integer
exponent. The math module has the
constants (math.PI, math.TAU, math.E, math.INFINITY, math.NAN,
and the epsilon and limits of float and double) and math.gcd and
math.lcm for integers:
# Floating-point math: trigonometry, logarithms, powers, classification,
# and the math module's constants and integer functions.
import math;
main(): int {
var angle = math.PI / 6;
assert((angle.sine() - 0.5).abs() < 1e-12, "sin 30 degrees is 1/2");
assert(((1.0).arc_tangent2(1.0) - math.PI / 4).abs() < 1e-12, "45 degrees");
assert((1024.0).logarithm2() == 10.0, "2 to the 10th is 1024");
assert((1.0).exponential() == math.E, "e to the 1st");
assert(2.0 ** 10 == 1024.0 && (-8.0).cube_root() == -2.0, "powers and roots");
assert((3.0).hypotenuse(4.0) == 5.0, "a 3-4-5 triangle");
assert(math.NAN.is_nan() && math.NAN != math.NAN, "NaN is not equal to itself");
assert(math.INFINITY.is_infinite() && !math.INFINITY.is_finite(), "infinite");
assert((-0.0).sign_bit() && !(-0.0 < 0), "negative zero");
assert(math.gcd(12, 18) == 6 && math.lcm(4, 6) == 12, "integers");
return 0;
}
Hints for the optimizer are [Likely] and
[Unlikely] on a
branch and the prefetches of the hint module. Any other LLVM intrinsic can be declared as an
extern; see LLVM intrinsics.
Operators
# Operators on numbers and booleans.
main(): int {
# Arithmetic.
assert(2 + 3 * 4 == 14, "* binds tighter than +");
assert((2 + 3) * 4 == 20, "parentheses group");
assert(17 % 5 == 2, "remainder");
assert(-(3) == -3, "negation");
# Exponentiation is right-associative: 2 ** 3 ** 2 is 2 ** 9.
assert(2 ** 10 == 1024, "power");
assert(2 ** 3 ** 2 == 512, "right-associative");
# With a floating-point operand, the result is floating point.
assert(2.0 ** -2 == 0.25, "float power");
assert(9 ** 0.5 == 3.0, "square root");
# Comparison yields a bool.
var small = 3 < 5;
assert(small, "3 < 5");
assert(5 >= 5 && 4 <= 5 && 6 > 5 && 5 == 5 && 5 != 6, "comparisons");
# On numbers, `===` and `!==` are the same as `==` and `!=`.
assert(5 === 5 && 5 !== 6, "identity on numbers");
# Logical operators; 'and' and 'or' are spellings of && and ||.
var t = true;
var f = false;
assert(t && !f, "and, not");
assert(f || t, "or");
assert(t and (f or t), "word forms");
# Bitwise operators.
assert((6 & 3) == 2, "and");
assert((6 | 3) == 7, "or");
assert((6 ^ 3) == 5, "exclusive or");
assert(~0 == -1, "complement");
assert(1 << 4 == 16 && 256 >> 2 == 64, "shifts");
# Compound assignment and increment.
var n = 5;
n += 3; # 8
n -= 2; # 6
n *= 4; # 24
n /= 5; # 4
n %= 3; # 1
n <<= 3; # 8
n >>= 1; # 4
n &= 6; # 4
assert(n == 4, "compound assignment");
n++;
++n;
n--;
assert(n == 5, "increment and decrement");
# The conditional (ternary) operator.
var sign = n > 0 ? "positive" : "not positive";
assert(sign == "positive", "ternary");
return 0;
}
| Kind | Operators |
|---|---|
| Arithmetic | + - * / % **, unary - and + |
| Comparison | == != < <= > >= |
| Identity | === !== (two references to the same object; plain equality for numbers and bool) |
| Logical | && (or and), || (or or), ! |
| Bitwise | & | ^ ~ << >> |
| Assignment | = += -= *= /= %= <<= >>= &= |= ^= |
| Increment | ++ --, prefix or postfix |
| Conditional | condition ? a : b |
&& and || short-circuit: the right operand is evaluated only when the left
one does not decide the result, so items.count > 0 && items[0] == 1 never
reads items[0] from an empty array.
Unary operators nest: -~x is -(~x), and !!x is !(!x), which turns a
number into a bool. A struct can define its own operators, the truth test
!! included (see Operators).
** raises a number to a power, and is right-associative: 2 ** 3 ** 2 is
2 ** 9. On two integers the result is an integer, and the exponent can be
any integer expression. A negative exponent gives the truncated real result: 1
for a base of 1, 1 or -1 for a base of -1, and 0 otherwise (2 ** -1 is 0).
When either operand is a float or double, the result is floating point, by
the usual conversions above: 2.0 ** 0.5 is the square root of 2, 2.0 ** -2
is 0.25, and 4 ** 0.5 is the double 2.0. An integer exponent of at most 32
bits computes by repeated multiplication (LLVM’s powi), which is fast but may
differ from C’s pow in the last bit; any other exponent calls pow itself.
** on anything but numbers or a struct that defines it is an error:
main.pep:3:15: error: invalid operands to binary expression ('__ptr<int>' and 'int')
3 | var q = p ** 2;
| ^~
Operators bind in this order, tightest first:
- Postfix: calls
f(x), indexinga[i], member accessa.b,x++,x-- **as,as?- Unary
+,-,!,~ - Prefix
++,-- *,/,%+,-<<,>>..,..>(ranges; not associative)is<,<=,>,>===,!=,===,!==&|,^&&,and||,or? :=and the compound assignments (right-associative)
As in C, & binds more loosely than ==, so write (flags & mask) == mask
with the parentheses.
An expression can nest at most 256 levels deep, counting each operator of a chain as one level, so a sum of more than about 250 terms is a compile error (see Compiler errors).
String interpolation
An interpolated string is a string literal with a $ before its opening
quote. Inside it, an expression in braces, {name} or {a + b}, is replaced
by the text of its value:
# Interpolated strings: `$"..."` with expressions in braces.
main(): int {
var name = "Pepper";
var count = 3;
var ready = true;
# Each {name} is replaced by that variable's text.
var line = $"{name} has {count} items, ready: {ready}";
assert(line == "Pepper has 3 items, ready: true", "integers, bool and String");
# Any expression fits between the braces, strings included.
var sizes = [2, 5];
assert($"total {sizes[0] + sizes[1]}" == "total 7", "arithmetic and index");
assert($"{name.size} letters" == "6 letters", "call");
assert($"{count > 1 ? "many" : "one"}" == "many", "ternary with strings");
# Doubled braces are literal braces; escapes work as in any string.
assert($"{{count}} = {count}" == "{count} = 3", "literal braces");
assert($"tab\t{count}\n" == "tab\t3\n", "escapes");
return 0;
}
{expression}holds an expression: a name (a local, a parameter, a global, or inside a method a field), a member access ({p.x}), a call ({f(x)}), an index ({items[0]}), arithmetic ({a + b}), or a ternary ({a > b ? a : b}). Spaces around it are ignored.- The expression changes no variable: an assignment (
=,+=, …),++or--anywhere in it is an error, even inside a call’s arguments ({f(a = 3)}). Only a closure’s body, which does not run in place, may hold one. A function it calls may still have effects of its own. - String literals may appear inside the braces (
{greet("you")}), and so may character literals and other interpolated strings. A}inside them, or inside brackets of the expression’s own, does not end it. - The expression fits on one line and holds no comment: a line break
or a
#inside the braces is an error. - A format specification may follow the expression after a
:, as in{n:>8}or{n:08x}: see below. {{and}}are a literal{and}.- Escapes (
\n,\t,\", …) work as in any string literal. - The result is a new
String. An interpolated string with no braces is an ordinary string.
Which values can be interpolated
Each value is converted with to_string(value), so it can be:
- an integer of any size, signed or unsigned, which gives its decimal text;
- a
floatordouble, which gives the shortest decimal text that reads back as the same number, as Swift prints one:0.1,3.0(a whole number keeps its.0),-2.5, and an exponent outside 1e-4 to 1e16 (1e+16,1.5e-05);nan,infand-inffor the special values; - a
bool, which givestrueorfalse; - a tuple of up to eight values that can each be
interpolated, which gives its elements’ text in parentheses,
(1, two)(aStringelement without quotes); - a
String, which is used as it is; - a value whose type has a
to_string(): Stringmethod, such as a struct that implements the runtime’sToStringtrait, or a value of typeToStringitself; - a value whose type has a
to_string<T>override. Likehash<Point>(see overriding a generic function), an override applies to the whole program, so it serves types that have noto_string()method of their own.
# A struct is interpolated through its to_string() method.
struct Point {
var x: int = 0;
var y: int = 0;
construct(this.x = 0, this.y = 0) {}
}
extend Point with ToString {
to_string(): String {
# Inside a method, fields are names too.
return $"({x}, {y})";
}
}
# A type without the method gets a to_string<T> override instead.
struct Celsius {
var degrees: int = 0;
construct(this.degrees = 0) {}
}
to_string<Celsius>(value: Celsius): String {
return $"{value.degrees} C";
}
main(): int {
var start = Point(1, 2);
var reading = Celsius(21);
assert($"from {start}" == "from (1, 2)", "ToString");
assert($"it is {reading}" == "it is 21 C", "override");
# A trait value converts through the value's own method.
var shown: ToString = start;
assert($"{shown}" == "(1, 2)", "trait value");
return 0;
}
Numbers and tuples, with the format specifications below:
# Floating-point numbers and tuples have text too.
main(): int {
var price = 4.5;
var ratio = 1.0 / 3.0;
assert($"{price}" == "4.5", "the shortest text that reads back");
assert($"{2.0}" == "2.0", "a whole number keeps its point");
assert($"{1.0e20}" == "1e+20", "large numbers use an exponent");
assert($"{ratio:.3}" == "0.333", "a precision rounds to that many digits");
assert($"[{price:8.2}]" == "[ 4.50]", "numbers align right");
assert($"{price:+08.2}" == "+0004.50", "with a sign and zeros");
var point = (3, "north");
assert($"{point}" == "(3, north)", "a tuple's elements in parentheses");
print("{0} at {1:.1}", point, price); # (3, north) at 4.5
return 0;
}
Runtime types such as String, StringBuilder, io.Path, Uri and
regex.Match implement ToString. An enum is
interpolated through a to_string() method it declares (return name();
gives the case’s name).
A plain overload such as to_string(p: Point) is not used: the runtime’s
code does not import your file, so it cannot see yours (see
overloading). Write a
to_string<Point> override or a method instead.
Format specifications
A : after the expression starts a format specification, which pads the
value’s text, writes an integer in another base, or rounds a floating-point
number to a number of digits. Its parts, all optional, come in this order:
{value:[[fill]align][+][0][width][.precision][type]}
| Part | Meaning |
|---|---|
align | < left, ^ centered (leaning left), > right. Without one, numbers go to the right and everything else to the left. |
fill | The character the padding is made of, written before align; a space by default. It can be any character but {, }, ", ' and #. |
+ | Writes a + before a number that is not negative. |
0 | Pads with zeros after the sign (-0042). It cannot be combined with align. |
width | The least number of characters the text takes; longer text is kept whole. |
.precision | For a float or double: the number of digits after the point, rounded to nearest, a tie to the even digit ({pi:.2} is 3.14, {0.125:.2} is 0.12; .0 leaves no point). At most 100. |
type | d decimal, x and X hexadecimal (lower and upper case), b binary, o octal. |
# Format specifications: padding, signs and other bases after a `:`.
main(): int {
var count = 42;
var name = "Pepper";
# Width and alignment pad any value; numbers go right by default.
assert($"[{count:6}]" == "[ 42]", "a number goes right");
assert($"[{name:10}]" == "[Pepper ]", "text goes left");
assert($"[{name:>10}]" == "[ Pepper]", "right");
assert($"[{name:*^10}]" == "[**Pepper**]", "centered with a fill");
# Integers take a sign, zero padding and a base.
assert($"{count:+}" == "+42", "a plus sign");
assert($"{count:05}" == "00042", "zero padding");
assert($"{255:x} {255:X} {5:b} {8:o}" == "ff FF 101 10", "bases");
assert($"0x{255:08x}" == "0x000000ff", "padded hexadecimal");
assert($"{-42:x}" == "-2a", "a negative number keeps its sign");
# The `:` after the whole expression starts the specification.
assert($"[{count > 1 ? "many" : "one":>5}]" == "[ many]", "a ternary");
return 0;
}
- Padding (
fill,alignandwidth) works for any value that can be interpolated: it pads the value’sto_stringtext, counting characters, not bytes. +and0need a number: an integer (of any size, signed or unsigned), or afloatordoublegiven a precision ({x:+08.2}). Atypeneeds an integer, and a precision afloatordouble. Using one with any other value is an error at that character, and a precision cannot be combined with atype.- A negative number keeps its sign in every base:
{-42:x}is-2a, not the bits of its two’s complement. - The
:that starts the specification is the one after the whole expression, so a ternary can be formatted as it is:{n > 1 ? "many" : "one":>5}.
Formatting by position: String.format
String.format takes a string literal whose braces name its other
arguments by position, {0} for the first, each with an optional format
specification, and returns the text:
# String.format: braces name the arguments by position.
main(): int {
var a = 2;
var b = 3;
assert(String.format("{0} + {1} = {2}", a, b, a + b) == "2 + 3 = 5", "in order");
# An index can repeat or come out of order, with its own specification.
assert(String.format("{1} before {0}", "first", "second") == "second before first", "reordered");
assert(String.format("{0} is {0:x} in hex", 255) == "255 is ff in hex", "repeated");
assert(String.format("[{0:>4}] {{literal}}", a) == "[ 2] {literal}", "padding and braces");
return 0;
}
- The format must be a string literal: the compiler reads it, so an
index out of range, an argument no brace names, or a bad specification
is an error at compile time.
{{and}}are literal braces, as in an interpolated string. - Each argument is evaluated once, from left to right, however many braces name it and in whatever order.
- An index can repeat, to show one value twice or in two forms
(
"{0} is {0:x} in hex"). - It is part of the compiler, not a function in the runtime: it cannot be stored in a variable or called through a function value.
printtakes the same arguments:print("{0} of {1}", done, total)prints whatString.formatmakes of them.
Errors
A value with no conversion is reported at its braces, with a note at its type:
main.pep:5:17: error: cannot interpolate a value of type 'Foo': it has no 'to_string()' method and no 'to_string' overload
5 | print($"f = {f}");
| ^~~
main.pep:1:8: note: declare 'to_string(): String' in 'Foo', or implement 'ToString' with 'extend Foo with ToString'
1 | struct Foo {}
| ^~~
A lone brace is an error, since doubling it is how to write one, and so is
anything after the expression but its }:
main.pep:2:15: error: unmatched '}' in interpolated string; write '}}' for a literal '}'
2 | print($"a } b");
| ^
main.pep:3:18: error: expected '}'
3 | print($"a {x b");
| ^
main.pep:3:15: note: to match this '{'
3 | print($"a {x b");
| ^
Empty braces are reported where the expression should start:
main.pep:2:16: error: expected an expression after '{' in interpolated string; write '{{' for a literal '{'
2 | print($"a {} b");
| ^
An error in the expression itself, such as a name that is not declared
(use of undeclared identifier 'x'), is reported like any other, at the
token inside the braces.
A format specification’s errors are reported at the character in it that is wrong:
main.pep:5:16: error: unexpected 'q' in format specification; expected '[[fill]align][+][0][width][.precision][type]'
5 | print($"{n:q}");
| ^
main.pep:5:19: error: cannot format a value of type 'String' as an integer
5 | print($"{name:x}");
| ^
main.pep:6:16: error: cannot format a value of type 'i32' with a precision; only 'float' and 'double' take one
6 | print($"{n:.2}");
| ^
String.format reports an index with no argument at the index, and an
argument with no index at the argument:
main.pep:5:31: error: argument index 2 is out of range for 2 arguments
5 | print(String.format("{0} {2}", n, n));
| ^
main.pep:5:35: error: argument 1 of 'String.format' is not used by its format string
5 | print(String.format("{0}", n, n + 1));
| ^~~~~
Changing a variable inside the braces is reported at the operator:
main.pep:3:16: error: an assignment cannot appear in an interpolated string
3 | print($"{a += 1}");
| ^~
How it works
The compiler turns $"({x}, {y + 1})" into "(" + to_string(x) + ", " + to_string(y + 1) + ")", resolving to_string in the runtime rather than
in your file. So a struct’s own to_string() method can interpolate its
fields without calling itself, and the result costs what the same +
expression does. A value with a format specification goes through a
runtime call that pads or converts its text instead. String.format becomes
the same concatenation, with its arguments bound to hidden names first when
the braces name them out of order or more than once.
Text encodings
A String always holds UTF-8. The text module converts text to and from
the bytes of an encoding (u8[]), for files, network protocols and C
libraries that use another one:
# Text in other encodings: encode a String to bytes, decode bytes back.
import text;
main(): int {
var greeting = "héllo 🙂";
# UTF-8 is what a String holds; decode checks the bytes.
var utf8 = text.Utf8.encode(greeting);
assert(utf8.count == 11, "é takes two bytes, 🙂 four");
assert(text.Utf8.decode(utf8).value() == greeting, "round trip");
# UTF-16: two bytes per unit, little-endian unless asked otherwise.
var utf16 = text.Utf16.encode(greeting);
assert(utf16.count == 16, "🙂 takes a surrogate pair");
assert(text.Utf16.decode(utf16).value() == greeting, "round trip");
# ASCII has no é: strict gives none, lossy writes '?'.
assert(text.Ascii.encode(greeting).is_empty(), "not ASCII");
assert(text.Ascii.encode_lossy("é").count == 1, "one '?'");
# Code page 437, as old zip archives name entries: é is 0x82, and every
# byte decodes, so decode gives a String.
var dos = text.CodePage437.encode("café").value();
assert(dos.count == 4 && dos[3] == 0x82, "one byte each");
assert(text.CodePage437.decode(dos) == "café", "round trip");
assert(text.CodePage437.encode(greeting).is_empty(), "no 🙂 in code page 437");
# Strict decoding rejects invalid bytes; lossy decoding replaces them.
var bad = Array<u8>();
bad.append(104);
bad.append(255);
assert(text.Utf8.decode(bad).is_empty(), "0xFF is never UTF-8");
assert(text.Utf8.decode_lossy(bad) == "h\u{FFFD}", "replaced");
return 0;
}
| Type | Encoding |
|---|---|
text.Utf8 | UTF-8. encode copies the String’s bytes; decode checks them. is_valid checks bytes without decoding, and find_invalid gives the offset of a String’s first invalid byte. code_point_at reads one code point and append_code_point writes one. |
text.Utf16 | UTF-16, two bytes per code unit, little-endian by default (encode(s, true) and decode(bytes, true) for big-endian). A code point past U+FFFF takes a surrogate pair. |
text.Ascii | ASCII: code points below 128, one byte each. is_ascii checks a String. |
text.CodePage437 | Code page 437, the IBM PC character set that old zip archives use for entry names: ASCII below 128, and one accented letter, Greek letter or box-drawing symbol for each byte above. Every byte decodes, so decode gives a String, not an Optional. |
decodeis strict: it gives Empty when the bytes are not valid in the encoding (a stray UTF-8 continuation byte, an unpaired UTF-16 surrogate, an odd UTF-16 byte count, an ASCII byte of 128 or more).decode_lossyalways succeeds: whatever is invalid becomes U+FFFD, the replacement character.Ascii.encodegives Empty for text with a code point past 127, andCodePage437.encodefor a character the code page lacks;encode_lossywrites?for each instead.- No byte-order mark is written, and one that is read stays in the text.
io.File.read_stringdecodes withtext.Utf8.decode, and an HTTP body’stext()withdecode_lossy.
The reference lists every function.
Regular expressions
The regex module finds patterns in text: test whether text matches,
find each match and its capture groups, and replace or split by a pattern.
# Regular expressions: match, capture, iterate, replace and split.
import regex;
main(): int {
# Compile once, reuse. Backslashes are doubled inside a string literal.
var date = regex.Regex("(?<year>\\d{4})-(\\d{2})-(\\d{2})");
assert(date.is_match("due 2026-10-01"), "a date somewhere");
# The first match, with its groups. Positions are byte offsets.
var found = date.find("due 2026-10-01").value();
assert(found.text() == "2026-10-01" && found.start() == 4, "where");
assert(found.group(2).value() == "10", "a numbered group");
assert(found.group("year").value() == "2026", "a named group");
# Every match, left to right.
var total = 0;
for (var number in regex.Regex("\\d+").find_all("1 22 333")) {
total += int.parse(number.text()).value();
}
assert(total == 356, "1 + 22 + 333");
# Replace with group references, and split.
var swapped = regex.Regex("(\\w+)@(\\w+)").replace_all("ann@home bob@work", "$2:$1");
assert(swapped == "home:ann work:bob", "groups swapped");
var parts = regex.Regex("\\s*,\\s*").split("a , b,c");
assert(parts.count == 3 && parts[1] == "b", "three pieces");
# Flags go inside the pattern: (?i) ignores case.
assert(regex.Regex("(?i)^hello").is_match("HeLLo there"), "any case");
# An invalid pattern from outside the program: parse reports it.
var bad = regex.Regex.parse("(unclosed");
assert(bad.is_err(), "missing ')'");
return 0;
}
Compiling a pattern
regex.Regex(pattern)compiles a pattern and panics when it is invalid. Use it for patterns written in the program.regex.Regex.parse(pattern)returns aResult<Regex>instead, whose error names the problem and its byte offset:invalid regex '(a': missing ')' at offset 0. Use it for patterns that come from input.- Compile once and reuse the
Regex; compiling costs more than a search. Regex.escape(text)escapes every character with a meaning in a pattern, so the result matchestextliterally.
A pattern is a String, so each backslash is doubled in a string literal:
"\\d+" is the pattern \d+.
Searching
| Method | Result |
|---|---|
is_match(text) | whether there is a match anywhere; the fastest test |
find(text) | the leftmost Match, or Empty |
find_at(text, start) | the leftmost match starting at or after byte start |
find_all(text) | an iterator over every match, left to right, without overlaps |
replace(text, replacement) | text with the first match replaced |
replace_all(text, replacement) | text with every match replaced |
split(text) | the pieces of text between matches |
A Match has text(), start() and end() (byte offsets, as String.find
returns, for use with substr_bytes), and group(index) or group(name),
which give the group’s text or Empty when the group did not take part (as
(a) in (a)|b matching b). Group 0 is the whole match; groups are
numbered by their opening (. group_range(index) gives a group’s offsets.
Matching is leftmost-first, as in Perl: the match that starts earliest wins,
and among those, a greedy repetition prefers more and alternation prefers
its earlier branch (a|ab finds a in ab).
find_all skips an empty match that comes right after the previous match,
so a* finds a twice in aba, not also an empty match between them.
In a replacement, $1 or ${1} stands for group 1, ${name} for a named
group, $0 for the whole match and $$ for a $. A group that did not take
part gives empty text. Use braces when a digit or letter follows:
${1}0, not $10.
Syntax
The syntax is RE2’s, the one Go’s regexp and Rust’s regex crate use.
| Syntax | Matches |
|---|---|
. | any code point except a newline (any at all with (?s)) |
[abc], [a-z], [^a-z] | a code point in, or not in, the set |
[[:alpha:]], [[:^digit:]] | a POSIX class: alnum, alpha, ascii, blank, cntrl, digit, graph, lower, print, punct, space, upper, word, xdigit |
\d, \w, \s | an ASCII digit, word character ([0-9A-Za-z_]), or space ([\t\n\f\r ]) |
\D, \W, \S | anything else |
\n, \t, \r, \f, \v, \a | a control character |
\x41, \x{1F642} | the code point with that hexadecimal value |
\., \*, \\, … | the punctuation character itself |
x*, x+, x? | zero or more, one or more, zero or one |
x{n}, x{n,}, x{n,m} | exactly n, at least n, from n to m (at most 1000) |
x*?, x+?, x??, x{n,m}? | the same, preferring fewer (lazy) |
xy, x|y | x then y; x or y |
(x) | a capture group |
(?<name>x), (?P<name>x) | a named capture group |
(?:x) | a group that does not capture |
^, $ | the start and end of the text (of a line with (?m)) |
\A, \z | the start and end of the text, always |
\b, \B | an ASCII word boundary, and anything else |
Flags go in the pattern: (?i) makes the rest of the enclosing group
case-insensitive, (?i:x) only x, and (?-i) turns a flag off.
| Flag | Meaning |
|---|---|
i | case-insensitive (ASCII letters) |
m | multi-line: ^ and $ also match at line breaks |
s | . also matches a newline |
U | swap greedy and lazy: x* is lazy and x*? greedy |
Text
- A pattern works on code points:
.and[^a]match one whole code point, such aséor🙂, never part of one. \d,\w,\s,\b, the POSIX classes and case-insensitive matching are ASCII-only:\wdoes not matché.- Positions are byte offsets, like
String.findandsubstr_bytes, not the code point positions ofsubstr.
Linear time
Matching never backtracks. The engine (a Pike VM, as in RE2) steps every
possible match through the text together, so a search takes time
proportional to the size of the text times the size of the pattern,
whatever both are. A pattern such as (a*)*b, which takes exponential
time in a backtracking engine, is safe on untrusted input.
The price is two features that need backtracking. A pattern that uses one fails to compile:
- Backreferences (
\1):backreferences are not supported - Lookaround (
(?=x),(?!x),(?<=x),(?<!x)):lookaround is not supported
Groups nest at most 250 deep, and a pattern whose compiled program would
exceed 100,000 instructions (from large counted repetitions such as
(a{1000}){1000}) is rejected.
Threads
A Regex never changes once it is compiled, and it is [Shared]: one
Regex can serve every thread at once, so a pattern the program uses
often can be compiled once at module scope:
import regex;
const identifier = regex.Regex("^[A-Za-z_][A-Za-z0-9_]*$");
is_identifier(text: String): bool {
return identifier.is_match(text);
}
Each search keeps its working state to itself, and a Match (which holds
the searched String) stays on the thread that found it.
The reference lists every method.
JSON
The json module reads JSON text into a JsonValue, lets you look values
up and change them, and writes them back as compact or indented text. It
also converts structs to JSON and back.
# JSON: parse text, look values up, change them, and write them back.
import json;
main(): int {
var text = "{\"name\": \"pepper\", \"server\": {\"port\": 8080}, \"tags\": [\"fast\"]}";
var parsed = json.parse(text, path = "config.json");
if (parsed.is_err()) {
print(parsed.error().to_string()); # config.json:1:17: error: ...
return 1;
}
var root = parsed.value();
# Lookups chain, and a missing key gives Null instead of panicking.
assert(root["name"].as_string().value() == "pepper", "a string");
assert(root["server"]["port"].as_int().value_or(80) == 8080, "a number");
assert(root["server"]["host"].as_string().value_or("localhost") == "localhost", "a default");
# Match on the case to handle every kind.
var described = match (root["tags"]) {
Array(items) => $"{items.count} tag",
Null => "no tags",
else => "not a list",
};
assert(described == "1 tag", "an array");
# Change the value in place, then write it back. Keys keep their order.
root["tags"].append(json.JsonValue.String("safe"));
root["debug"] = json.JsonValue.Bool(false);
assert(root.to_string() == "{\"name\":\"pepper\",\"server\":{\"port\":8080},\"tags\":[\"fast\",\"safe\"],\"debug\":false}", "compact");
# Build a value from scratch.
var reply = json.JsonValue.object();
reply["id"] = json.JsonValue.Integer(42);
reply["ok"] = json.JsonValue.Bool(true);
assert(reply.to_pretty_string() == "{\n \"id\": 42,\n \"ok\": true\n}", "indented");
# An error names the line and column.
var bad = json.parse("{\n \"a\": 1\n \"b\": 2\n}", path = "bad.json");
assert(bad.error().to_string() == "bad.json:3:3: error: expected ',' or '}'", "where");
# JSONC: comments and trailing commas, on request.
var settings = json.parse("{\n // the theme\n \"theme\": \"dark\",\n}", comments = true);
assert(settings.value()["theme"].as_string().value() == "dark", "JSONC");
return 0;
}
Values
json.JsonValue is an enum whose cases carry values, so
match handles each kind:
| Case | Holds | JSON |
|---|---|---|
Null | nothing | null |
Bool(value: bool) | the boolean | true, false |
Integer(value: long) | an integer within ±(2^53 − 1) | 42, -7 |
Number(value: double) | any other number | 2.5, 1e3, 9007199254740993 |
String(value: String) | the string | "hi" |
Array(items: Array<JsonValue>) | the elements | [1, 2] |
Object(members: OrderedMap<String, JsonValue>) | the members, in insertion order | {"a": 1} |
- Numbers keep their form. A number written without a fraction or an
exponent is an
Integerwhen it is within ±(2^53 − 1), the range every JSON implementation agrees on (RFC 7493). Any other number, including a larger integer, is aNumber. - Make values with the cases (
JsonValue.Integer(42),JsonValue.String("a"),JsonValue.Null), and containers withJsonValue.object()andJsonValue.array(items). - Containers are shared. An
ArrayorObjectholds its array or map by reference, so changing it throughvalue[key] = ...,append, or the map fromas_object()changes every holder of the value.
Looking values up
| Expression | Result |
|---|---|
value["key"] | the member, or Null when missing or not an object |
value[index] | the element, or Null when out of range or not an array |
value.as_string(), as_bool(), as_double() | the value, or Empty for another kind |
value.as_int() | an Integer, or a Number with no fraction that fits in a long; otherwise Empty |
value.as_array(), as_object() | the array or map itself, or Empty |
value.is_null() | whether it is Null |
value.contains("key") | whether an object has the member, even a null one |
A lookup that finds nothing gives Null rather than panicking, so lookups
chain and a default goes at the end:
root["server"]["port"].as_int().value_or(8080).
Changing values
value["key"] = membersets an object’s member. A new key goes at the end; an existing key keeps its place. It panics on a value that is not an object.value[index] = elementreplaces an array’s element, andvalue.append(element)adds one at the end. Both panic on a value that is not an array.value.remove("key")removes an object’s member and says whether it was there.
Parsing
json.parse(text) returns a Result<JsonValue, JsonError>. It is strict
RFC 8259:
- The text holds one value, with only spaces, tabs and line breaks around it. A byte-order mark at the start is skipped.
- The text must be valid UTF-8; an invalid byte is an error at that byte.
- A string cannot hold a raw control character (write
\n), and a\uescape of an unpaired surrogate ("\uD800") is an error. - No comments, trailing commas, single quotes,
NaN, leading zeros (01) or+signs. - When an object repeats a key, the last value wins, in the place of the first.
parse takes optional arguments, usually passed by name:
| Argument | Default | Meaning |
|---|---|---|
comments | false | Accept JSONC, as VS Code’s settings files are: // and /* */ comments, and a comma after the last element or member. |
path | "" | The name of the text, which the error’s to_string() puts first. |
max_depth | 1000 | How deeply arrays and objects may nest. |
The parser keeps its own stack rather than recursing, so deep nesting in the
text cannot overflow the call stack. max_depth still bounds it, because
releasing a value releases its nested values recursively. Raise it with
care: Windows gives a program’s main thread a 1 MB stack, which a few
thousand levels of nested objects can use up when the value is released.
Errors
A JsonError has the message, the line and column (from 1, counting
bytes) and the byte offset. to_string() renders it like a compiler
error:
config.json:3:3: error: expected ',' or '}'
Without a path, it starts at the line: 3:3: error: expected ',' or '}'.
Writing
value.to_string()writes compact text, with no spaces or line breaks:{"id":42,"tags":["a","b"]}. String interpolation uses it too.value.to_pretty_string()puts each element and member on its own line, indented two spaces per level (to_pretty_string(indent = 4)for four), with a space after each:. An empty array or object stays[]or{}.- Strings are written as UTF-8, escaping
",\and the control characters.to_string(ascii_only = true)andto_pretty_string(ascii_only = true)also escape everything outside ASCII as\u, with a surrogate pair past U+FFFF. - A
Numberis written as Pepper writes adouble(0.5,1000.0,1e+16), so it reads back as aNumber. NaN and the infinities, which JSON cannot spell, are written asnull. - Objects keep their keys in insertion order, so parsing a document and writing it back keeps its order.
Documents with comments
parse skips a JSONC file’s comments. To change a file people edit, such
as a settings file, and write it back with its comments, read it with
json.parse_document(text), which returns a Result<JsonDocument, JsonError>:
# JSON: change a JSONC settings file and write it back with its comments.
import json;
main(): int {
var text = "{\n // Spaces per level.\n \"tab_size\": 4,\n \"font\": \"Mono\", // the editor's\n \"wrap\": true\n}";
var parsed = json.parse_document(text, path = "settings.json");
if (parsed.is_err()) {
print(parsed.error().to_string());
return 1;
}
var document = parsed.value();
assert(document.to_string() == text, "unchanged text comes back as it was");
# Change the value; each comment stays with its value.
document.root["tab_size"] = json.JsonValue.Integer(2);
document.set_comments("$.wrap", ["// Wrap long lines."]);
assert(document.comments("$.tab_size")[0] == "// Spaces per level.", "by path");
print(document.to_string());
# {
# // Spaces per level.
# "tab_size": 2,
# "font": "Mono", // the editor's
# // Wrap long lines.
# "wrap": true
# }
return 0;
}
document.rootis theJsonValue; change it as any other.- Each comment belongs to a value, by its path from the root
$($.editor.tab_size,$.files[0]): the comments on the lines before it, and those on the line it ends, after its comma. Comments before a closing bracket stay at the end of their array or object, and those around the whole value stay before or after it. document.to_string()writes the text indented, asto_pretty_stringdoes (to_string(indent = 4)for four spaces), with every comment back in its place. A file in that layout reads and writes back unchanged.- A comment whose value is removed is dropped. One whose path later names another value, such as an element shifted into its index, goes with that value.
document.comments(path)returns the comments before a value, anddocument.set_comments(path, ["// ..."])replaces them.parse_documenttakespathandmax_depthasparsedoes, and always accepts JSONC.
Structs
json.encode(value) writes a struct as JSON text, and
json.decode<T>(text) reads one back. There is no code to write per type:
both walk the struct’s properties with reflection while
compiling, so the conversion costs what hand-written code would.
# JSON: convert structs to JSON text and back, through reflection.
import json;
enum Mode {
Fast,
Safe,
}
struct Tls {
var cert: String;
}
struct ServerConfig {
var host: String;
[Required]
var port: int;
# The key is "max-connections", not the property's name.
[Name("max-connections")]
var max_connections: int;
# Never written or read.
[Ignore]
var started_at: long;
# `null` or missing when none.
var tls: Tls?;
var mode: Mode;
var tags: Array<String>;
}
main(): int {
var config = ServerConfig();
config.host = "localhost";
config.port = 8080;
config.max_connections = 64;
config.mode = Mode.Safe;
config.tags = ["web"];
var text = json.encode(config);
assert(
text == "{\"host\":\"localhost\",\"port\":8080,\"max-connections\":64,\"tls\":null,\"mode\":\"Safe\",\"tags\":[\"web\"]}",
"properties in declaration order"
);
var decoded = json.decode<ServerConfig>("{\"port\": 443, \"tls\": {\"cert\": \"site.pem\"}}");
if (decoded.is_err()) {
print(decoded.error().to_string());
return 1;
}
var back = decoded.value();
assert(back.port == 443, "a number");
assert(back.tls.value().cert == "site.pem", "a nested struct");
assert(back.mode == Mode.Fast, "a missing key keeps its default");
# An error names where in the document it is, by path, line and column.
var bad = json.decode<ServerConfig>("{\"port\": 443, \"mode\": \"Slow\"}", path = "server.json");
assert(bad.error().to_string() == "server.json:1:23: error: $.mode: 'Slow' is not a case of 'Mode'", "a path");
var missing = json.decode<ServerConfig>("{}");
assert(missing.error().to_string() == "1:1: error: $: missing required key 'port'", "[Required]");
return 0;
}
A struct’s public properties become an object’s members,
in declaration order. Each property’s type converts as follows, and the
same rules apply at the top level (json.decode<Array<int>>("[1, 2]")):
| Pepper | JSON |
|---|---|
an integer type (int, u8, long, …) | a number; reading one checks that it has no fraction and fits the type |
float, double | a number |
bool | true, false |
String | a string |
| an enum | a case that carries no values as its name ("Safe"); one that carries values as an object with the case’s name as its one key, holding the values by name ({"Circle": {"radius": 2}}); attributes choose other forms |
T? | the value, or null when Empty; a missing key reads as Empty |
Array<T> | an array |
HashMap<String, V>, OrderedMap<String, V>, Map<String, V> | an object |
| a struct | an object, recursively |
json.JsonValue | itself, for parts of a document with no fixed shape |
A struct reference that holds nothing (a String, array, struct or
reference enum field never assigned) is written as null, and null reads
back as one, so a value round-trips. Reading a case’s value whose key is
missing is an error: $.shapes[0].Circle: missing value 'radius'. null for a number, bool or enum is an error.
Attributes
The serialize module’s attributes, which json uses (they need no
import of their own), change how a property is converted:
| Attribute | Effect |
|---|---|
[Name("key")] | writes and reads the property under key |
[Ignore] | leaves the property out of both |
[Required] | makes a missing key an error when reading |
On an enum and its cases, they choose how a case is written:
| Attribute | Effect |
|---|---|
[Name("key")] on a case | writes and reads the case under key instead of its own name |
[Tag("kind")] on an enum | writes each case as an object naming it under kind, with its values beside the tag: {"kind": "Circle", "radius": 2}, {"kind": "Point"} |
[Tag("kind", content = "data")] on an enum | puts the values in an object under data instead: {"kind": "Circle", "data": {"radius": 2}} |
[Untagged] on an enum | writes no tag: a case’s values as an object ({"radius": 2}), and a case that carries none as its name ("Point") |
# JSON: choose how an enum's cases are written with attributes.
import json;
# A tag beside the values: {"kind": "circle", "radius": 2}.
[Tag("kind")]
enum Shape {
[Name("circle")]
Circle(radius: int),
Point,
}
# The values under their own key: {"type": "Click", "data": {"x": 1, "y": 2}}.
[Tag("type", content = "data")]
enum Event {
Click(x: int, y: int),
Close,
}
# No tag; reading takes the first case that fits.
[Untagged]
enum Size {
Fixed(px: int),
Auto,
}
main(): int {
assert(json.encode(Shape.Circle(2)) == "{\"kind\":\"circle\",\"radius\":2}", "internally tagged");
assert(json.encode(Shape.Point) == "{\"kind\":\"Point\"}", "a case with no values");
assert(json.encode(Event.Click(1, 2)) == "{\"type\":\"Click\",\"data\":{\"x\":1,\"y\":2}}", "adjacently tagged");
assert(json.encode(Size.Fixed(8)) == "{\"px\":8}", "untagged");
assert(json.encode(Size.Auto) == "\"Auto\"", "untagged, no values");
var shape = json.decode<Shape>("{\"kind\": \"circle\", \"radius\": 5}");
if (shape.value() is Circle(radius)) {
assert(radius == 5, "read back");
} else {
return 1;
}
var size = json.decode<Size>("\"Auto\"");
assert(size.value() is Auto, "the first case that fits");
var bad = json.decode<Shape>("{\"radius\": 5}");
assert(bad.error().to_string() == "1:1: error: $: missing tag 'kind'", "a missing tag");
return 0;
}
Reading an [Untagged] enum tries each case in order and takes the first
that reads without an error (with UnknownKeys.Reject, an extra key is
an error too), so a case whose values include another’s goes first. When
none reads, the error says so: $: found an object, which matches no case of 'Size'. A [Tag] enum’s errors name its tag: $: missing tag 'kind',
$.kind: 'Hex' is not a case of 'Shape'. A tag with the name of one of a
case’s values is a compile error, since both would be written under one
key.
Reading
decode makes a struct with its constructor that takes no arguments, then
reads in each property it finds a key for. A missing key leaves its
property at its default, unless the property is [Required]. It takes
optional arguments, usually passed by name:
| Argument | Default | Meaning |
|---|---|---|
unknown_keys | UnknownKeys.Ignore | What to do with a key that names no property: skip it, or with UnknownKeys.Reject, fail naming it. |
comments | false | Accept JSONC, as parse does. |
path | "" | The name of the text, which the error’s to_string() puts first. |
An error has a line and column as in Errors. One in
converting a value also has its value_path, where in the document the
value is as a path from the root $, and its message starts with that
path. The line and column are the value’s, or the enclosing object’s for a
missing key:
server.json:3:13: error: $.server.port: expected a number, found a string
server.json:7:21: error: $.tags[2]: expected a string, found null
server.json:4:24: error: $["max-connections"]: 70000 does not fit in 'u16'
server.json:1:1: error: $: missing required key 'port'
json.to_json(value) and json.from_json<T>(value) do the same
conversions to and from a JsonValue rather than text (with no text, a
from_json error has no line: error: $.port: ...), and
json.encode(value, indent = 2) writes indented text.
Types it cannot convert
A property whose type has no JSON form, such as a pointer, a function, or
a Result (a result is not a value to send over the wire), is a compile
error naming the property. Notes follow the conversion back to the call
that asked for it:
runtime/json/convert.pep:358:25: error: 'encode' cannot convert property 'outcome' of type 'Result<int>' to JSON
...
main.pep:10:16: note: 'encode<Job>' is used by 'main' here
10 | var text = json.encode(job);
| ^~~~~~~~~~~
Current limits
parseskips comments; only aJsonDocumentkeeps them, anddecodenever does. A document is always written indented.- A struct’s own conversion cannot be customized yet: a type converts as its properties do.
- Map keys must be
Strings.
The reference lists every function.
TOML
The toml module reads TOML 1.0 into a
TomlValue, lets you look values up and change them, writes tables back
as documents, edits a document in place without disturbing its comments,
and converts structs to TOML and back. The package manager reads and
edits pep.toml and pep.lock with it.
# TOML: parse a document, look values up, edit it in place keeping its
# comments, and read it into a struct.
import toml;
struct Package {
[Required]
var name: String;
var version: String;
}
struct Manifest {
[Required]
var package: Package;
}
main(): int {
var text = "[package]\nname = \"demo\" # the name\nversion = \"0.1.0\"\n";
var parsed = toml.parse(text, path = "pep.toml");
if (parsed.is_err()) {
print(parsed.error().to_string()); # pep.toml:2:6: error: ...
return 1;
}
var root = parsed.value();
assert(root["package"]["name"].as_string().value() == "demo", "a string");
assert(root.lookup("package.version").value().as_string().value() == "0.1.0", "a dotted lookup");
assert(root.try_get("dependencies").is_empty(), "a missing key");
# Change a value, then write the tables back as a document.
var package = root["package"];
package["edition"] = toml.TomlValue.Integer(2026);
assert(root.to_document() == "[package]\nname = \"demo\"\nversion = \"0.1.0\"\nedition = 2026\n", "a document");
# Edit the text in place: comments and layout stay as written.
var document = toml.parse_document(text, path = "pep.toml").value();
document.set(["dependencies", "yaml"], toml.TomlValue.String("0.3")).value();
assert(
document.to_string() == "[package]\nname = \"demo\" # the name\nversion = \"0.1.0\"\n\n[dependencies]\nyaml = \"0.3\"\n",
"an edit"
);
# Read it into a struct, with the serialize attributes json uses.
Manifest manifest = toml.decode<Manifest>(text).value();
assert(manifest.package.version == "0.1.0", "a struct");
# An error names the line and column.
var bad = toml.parse("name \"demo\"\n", path = "bad.toml");
assert(bad.error().to_string() == "bad.toml:1:6: error: expected '=' after the key", "where");
return 0;
}
Values
toml.TomlValue is an enum whose cases carry values:
| Case | Holds | TOML |
|---|---|---|
String(value: String) | the string | "hi", 'C:\path', """...""" |
Integer(value: long) | a 64-bit integer | 42, 0xff, 1_000 |
Float(value: double) | a float, inf and nan included | 0.5, 1e10, inf |
Boolean(value: bool) | the boolean | true, false |
DateTime(value: TomlDateTime) | a date, a time, or both | 1979-05-27T07:32:00Z, 1979-05-27, 07:32:00 |
Array(items: Array<TomlValue>) | the elements | [1, "two"] |
Table(members: OrderedMap<String, TomlValue>) | the keys, in the order written | [server], { x = 1 } |
- Look values up with
value["key"]andvalue[index], which panic on a miss, ortry_get("key")andlookup("package.name"), which give an emptyOptional. Theas_methods (as_string,as_int,as_float,as_bool,as_date_time,as_array,as_table) give an emptyOptionalfor another kind;as_floatalso reads an integer. - Write a value on one line with
to_string()(as after a key’s=), or a table as a whole document withto_document(). - There is no null. What
jsonwrites asnullis left out, and a missing key reads as the property’s default.
Errors
toml.parse(text, path = "pep.toml") checks every rule of TOML 1.0 and
gives a TomlError that renders like a compiler error:
pep.toml:4:12: error: expected '=' after the key. Arrays and inline
tables may nest max_depth deep (1000 by default); parsing and writing
use explicit stacks, so deep nesting never overflows the stack. The
module passes all 709 TOML 1.0 cases of
toml-test, which its tests run.
Editing a document in place
toml.parse_document(text) gives a TomlDocument, whose set(keys, value) and remove(keys) change only the lines they must, keeping every
comment and blank line, the way cargo add edits Cargo.toml. A changed
value is rewritten on one line; a new key goes after the last key of its
table, indented like it; a new table goes at the end as a [header]
section, or as { ... } with inline = true. Edits cannot reach inside an
array of tables ([[bin]]); set or remove the whole array.
Structs
toml.encode(value) and toml.decode<T>(text) (and to_toml and
from_toml, which work on a TomlValue) convert structs through
reflection, with the serialize
attributes JSON uses: [Name("key")], [Ignore] and
[Required]. unknown_keys = toml.UnknownKeys.Reject makes an unknown key
an error. See the reference for every
function.
YAML
YAML 1.2 is read and written by the
yaml package, which has
the same shape as the json and toml modules: a
YamlValue to look values up in and change, clang-style errors at the
line and column, documents that keep their comments when edited, and
struct conversion with the serialize
attributes. It is a package rather than part of the runtime because few
programs need YAML; add it with
pepper package add justinboswell/pepper-yaml --name yaml.
import yaml;
main(): int {
var parsed = yaml.parse("server:\n port: 8080\n debug: no\n", path = "config.yaml");
if (parsed.is_err()) {
print(parsed.error().to_string());
return 1;
}
var root = parsed.value();
print(root["server"]["port"].as_int().value()); # 8080
print(root["server"]["debug"].as_string().value()); # no
root["server"]["port"] = yaml.YamlValue.Integer(9090);
print(root.to_string());
return 0;
}
- The core schema:
yes,no,onandoffare strings, not booleans, soNO(Norway) staysNO. - Safe for untrusted text: only the core schema’s tags (
!!str,!!int, …) are read, and any other tag is an error; aliases may not stand for more thanmax_alias_nodesnodes, which stops the “billion laughs” attack. - Merge keys:
yaml.parse(text, merge_keys = true)applies YAML 1.1’s<<(<<: *defaults), which CI and compose files use to reuse a mapping; the mapping’s own keys win, and merged nodes count towardmax_alias_nodes. By default<<is an ordinary key, as YAML 1.2 says. - Several documents in one stream (
---) are read byyaml.parse_alland written byyaml.write_all. yaml.parse_documentkeeps comments, key order, flow or block style and scalar styles through edits, asjson.parse_documentkeeps a JSONC file’s comments.yaml.encodeandyaml.decode<T>convert structs exactly asjson.encodeandjson.decodedo.
The package’s README lists what it reads and its limits: a mapping’s keys are strings, and there is no streaming reader yet.
Compression and archives
The compress module compresses and decompresses DEFLATE data, plain or
in its zlib and gzip framings, and the archive module reads and writes
tar archives. Together they handle .gz and .tar.gz files. Both are
written in Pepper, so a program that uses them links no extra library,
unless it is built to use zlib (see Using zlib).
# Compress and decompress bytes in one call, or as streams.
import compress;
import io;
main(): int {
var text = "the cat sat on the mat; ".repeat(100).bytes();
# gzip, zlib and raw DEFLATE each have compress and decompress.
var packed = compress.Gzip.compress(text);
assert(packed.count < text.count / 10, "repetitive text shrinks");
assert(compress.Gzip.decompress(packed).value() == text, "round trip");
# Level 0 stores, 9 squeezes hardest; 6 is the default.
var fast = compress.Zlib.compress(text, level = 1);
assert(compress.Zlib.decompress(fast).value() == text, "any level reads back");
# Corrupt data is an error, never a crash.
packed[packed.count - 5] = packed[packed.count - 5] ^ 1;
var bad = compress.Gzip.decompress(packed);
assert(bad.is_err() && bad.error().is_invalid_input(), "the CRC-32 check fails");
# Streams: a CompressWriter wraps any io.Writer, a DecompressReader any
# io.Reader, such as a File. Here both are in memory.
var sink = io.ByteWriter();
var writer = compress.CompressWriter(sink, compress.Format.Gzip);
writer.print_line("first line");
writer.print_line("second line");
writer.finish();
var reader = compress.DecompressReader(io.ByteReader(sink.bytes), compress.Format.Gzip);
var lines = 0;
for (var line in reader.lines()) {
lines += 1;
}
assert(lines == 2, "two lines");
# The checksums are there on their own too.
assert(compress.Crc32.of("123456789".bytes()) == 0xCBF43926, "CRC-32");
return 0;
}
Formats
| Format | Struct | Where it is used |
|---|---|---|
Format.Gzip | compress.Gzip | .gz files, HTTP Content-Encoding: gzip |
Format.Zlib | compress.Zlib | PNG images, HTTP deflate |
Format.Raw | compress.Deflate | zip entries, other containers with their own framing |
compress(data, level = 6)returns the compressed bytes. The level runs from 0 (stored, no compression) to 9 (smallest, slowest). Each level searches for matches with zlib’s settings for it, so the output is about the size zlib makes at the same level, at a similar speed: on 12 MB of source text, compressing at level 6 takes about 1.2 times as long as thegzipcommand, and decompressing is a little faster thangzip -d.decompress(data)returns anio.IoResult<u8[]>. Damaged or truncated data is an error whoseis_invalid_input()holds, and gzip and zlib data is checked against its CRC-32 or Adler-32 checksum.- Gzip input may hold several members one after another (
cat a.gz b.gz); they decompress to their contents joined. Bytes after the last member are an error. A gzip header’s file name and comment are skipped, and a written header has neither. - A zlib stream made with a preset dictionary needs it to decompress; see Preset dictionaries.
decompressputs everything in memory. Use aDecompressReaderfor input from outside the program, so a small file that expands hugely cannot exhaust memory.
compress.Crc32 and compress.Adler32 compute the two checksums over
data given in pieces (update, then value), or in one call with of.
Streams
compress.DecompressReader(source, format) is an io.Reader that
decompresses source, any io.Reader, as it is read, so lines(),
chunks(size) and read_all() work on it. It holds at most about
225 KiB of input and output, however large the data.
compress.CompressWriter(sink, format, level = 6) is an io.Writer that
compresses what is written and writes the result to sink. Call
finish() at the end: it writes the last block and the checksum. Dropping
the writer does not finish it, and finish() does not close the sink.
io.ByteReader(bytes) and io.ByteWriter() stand in for a stream when
the data is in memory.
Preset dictionaries
A short message compresses poorly, since the stream has no earlier bytes to refer back to. A preset dictionary gives it some: bytes that both sides agree on beforehand, such as the keys and common values of a JSON protocol, which the compressed data can then copy from as if they came just before it.
# Compress small messages against a preset dictionary of what they share.
import compress;
main(): int {
# Short messages with the same keys compress poorly on their own: the
# stream has no earlier bytes to refer back to.
var dictionary = "{\"user\": \"\", \"action\": \"login\", \"status\": \"ok\"}".bytes();
var message = "{\"user\": \"ada\", \"action\": \"login\", \"status\": \"ok\"}".bytes();
var plain = compress.Zlib.compress(message);
var primed = compress.Zlib.compress(message, dictionary);
assert(primed.count < plain.count, "the dictionary supplies the repeated text");
# Decompressing needs the same dictionary; the stream names it by its
# Adler-32, so a missing or different one is an error.
assert(compress.Zlib.decompress(primed, dictionary).value() == message, "round trip");
assert(compress.Zlib.decompress(primed).is_err(), "no dictionary given");
print($"{plain.count} bytes alone, {primed.count} with the dictionary");
return 0;
}
Zlib.compress(data, dictionary, level = 6)andZlib.decompress(data, dictionary)take one. The zlib header names the dictionary by its Adler-32, sodecompresswithout one, or with a different one, is anis_invalid_input()error. A stream made without a dictionary decompresses as usual when one is given.CompressWriter(sink, format, dictionary, level = 6)andDecompressReader(source, format, dictionary)take one forFormat.ZliborFormat.Raw. Raw DEFLATE data carries no sign of it, so both sides must know to use it. Gzip has no dictionary, and asking for one panics.- Only the last 32 KiB of a dictionary can be referred to, since that is how far back DEFLATE reaches; put the most common text at its end.
Using zlib
Building with -D USE_ZLIB (see Build options) makes
compress use zlib, the reference DEFLATE library, for compressing,
decompressing, CRC-32 and Adler-32, in place of its Pepper code:
pep-build main.pep -D USE_ZLIB -o app
- The API is the same, and so are the errors for damaged data, worded as the Pepper code words them where zlib tells the causes apart. Compressed output can differ byte for byte (zlib’s gzip header names the operating system), but each side decompresses the other’s.
- Linux links
-lz, from the development package (zlib1g-dev,zlib-devel). macOS has zlib already. Windows has none: install it (vcpkg install zlib) and name the folder holdingzlib.libwith-LorPEPPER_LIBRARY_PATH. - zlib compresses faster than the Pepper code at the same level, and makes output of about the same size.
Tar archives
# Write a .tar.gz, list it, and extract it.
import archive;
import compress;
import io;
main(): int {
var folder = io.Directory.temp().join("pepper-compression-tar-example");
io.Directory.remove_all(folder);
var path = io.Directory.temp().join("pepper-compression-tar-example.tar.gz");
# A TarWriter over a gzip CompressWriter over a File.
var file = io.File.create(path).value();
var gzip = compress.CompressWriter(file, compress.Format.Gzip);
var tar = archive.TarWriter(gzip);
tar.append_directory("app-1.0");
tar.append_file("app-1.0/main.pep", "main(): int { return 0; }\n".bytes());
tar.append_file("app-1.0/run.sh", "#!/bin/sh\n".bytes(), mode = 493);
tar.finish();
gzip.finish();
file.close();
# List the entries.
var reader = archive.TarReader(compress.DecompressReader(io.File.open(path).value(), compress.Format.Gzip));
var names = Array<String>();
while (true) {
var entry = reader.next_entry().value();
if (entry.is_empty()) {
break;
}
names.append(entry.value().path);
}
assert(names.count == 3 && names[0] == "app-1.0/", "a directory ends with '/'");
# Extract without the top directory.
reader = archive.TarReader(compress.DecompressReader(io.File.open(path).value(), compress.Format.Gzip));
reader.extract(folder, strip_components = 1).value();
assert(io.File.read_to_string(folder.join("main.pep")).value().starts_with("main"), "extracted");
io.Directory.remove_all(folder);
io.File.remove(path);
return 0;
}
archive.TarReader(source) reads an archive from any io.Reader;
wrap the file in a DecompressReader to read a .tar.gz.
next_entry()moves to the next entry and returns itsTarEntry:path,kind(File,Directory,Symlink,HardLinkorOther),size,mode,modified(seconds since 1970) andlink_target. It returns Empty at the end of the archive.- The reader is an
io.Readerof the current entry’s contents:read,read_all(),lines(). Contents not read are skipped. - It reads ustar archives with the pax and GNU extensions for long paths,
which covers GNU tar, bsdtar, and the
.tar.gzsnapshots thatgit archive, GitHub, GitLab and Codeberg make.
extract(target, strip_components = 0) writes the rest of the archive into
the directory target:
- It refuses, with an
is_invalid_input()error, an entry whose path is absolute or holds a..component, a\or a:, so an archive cannot write outsidetarget.archive.safe_components(path)is that check, for readers of other formats: it gives the path’s components, or Empty for an unsafe path. strip_componentsdrops leading path components, as tar’s--strip-componentsdoes. A GitHub snapshot puts everything undername-version/, whichstrip_components = 1removes.- Files and directories are created, replacing a file already there. A hard link becomes a copy of the file it names. Devices and FIFOs are skipped.
- Each file and directory gets the entry’s modification time and, on Linux
and macOS, its permission bits less the umask, as
tardoes, so a script keeps its executable bit. Setuid, setgid and sticky bits are never applied. A directory gets its mode and time after its contents are written, so a read-only directory can still be filled; one that already existed keeps its own. Windows and WASI apply no permission bits. - Symlinks are made after everything else, so no file is written through
one. A link whose target is absolute or climbs out of
targetwith.., or that would be made inside another symlink, is skipped. Windows, where making a symlink needs Developer Mode or an administrator, skips every symlink.
archive.TarWriter(sink) writes an archive: append_file(path, data),
append_directory(path), append_symlink(path, target), append(entry, data) for any header, and append_stream(entry, source) to copy
entry.size bytes from a stream. finish() writes the end marker.
Paths longer than a ustar header holds get a pax header, so none is cut
short.
Zip archives
.zip archives are read and written by the
zip package, which builds
on compress and archive. It is a package rather than part of the
runtime because the package manager needs only .tar.gz; add it with
pepper package add justinboswell/pepper-zip --name zip.
import zip;
main(): int {
var writer = zip.Writer.create("bundle.zip").value();
writer.add("version.txt", "1.0.0".bytes()).value();
writer.finish().value();
var archive = zip.Archive.open("bundle.zip").value();
for (var entry in archive.entries()) {
print($"{entry.name}: {entry.size} bytes");
}
archive.extract_all("out").value();
return 0;
}
- It reads and writes stored and DEFLATE entries, ZIP64 (archives and
entries past 4 GiB, more than 65,535 entries), and UTF-8 names; names in
older archives are decoded as code page 437 (
text.CodePage437). - Entries compressed another way (bzip2, LZMA, zstd) or encrypted are listed, and reading one is an error naming the entry and the method.
extract_allchecks every name witharchive.safe_componentsbefore writing anything, and each entry’s CRC-32 as it is written.
Statements and semicolons
Every simple statement ends with a ;: a variable declaration, an
expression, return, break and continue. So does every declaration that
does not end in a block: a module variable, a struct field, a type alias, an
import, a re-export and an extern function. A statement or declaration that
ends in a block (if, while, for, with, a function, a struct) needs
none.
Line breaks, spaces and comments mean nothing to the grammar. A statement
can span as many lines as it needs, breaking anywhere between tokens,
including before an operator or a ., and several statements can share a
line.
# Every simple statement ends with a `;`; line breaks mean nothing.
is_small(n: int): bool {
return n > 0 && n < 10;
}
main(): int {
var width = 3;
var height = 4; var depth = 5;
# A statement may span several lines, breaking before or after an
# operator.
var volume = width * height
* depth;
assert(volume == 60, "3 * 4 * 5");
var in_range = is_small(width)
&& is_small(height)
&& !is_small(volume);
assert(in_range, "3 and 4 are small; 60 is not");
var words = Array<String>();
words.append("line");
words.append("breaks");
var long_words = 0;
# A chain of calls may break before each `.`.
for (var word in words
.iterate()
.filter((w) => w.length > 4)) { # comments are fine anywhere
long_words += 1;
}
assert(long_words == 1, "one word is longer than four characters");
# So may a call's arguments.
assert(volume + words[0].length == 64,
"60 + 4");
return 0;
}
A missing ; is an error
Leaving out a ; is an error, reported right after the statement it should
end, with a fix that adds it. The language server offers the fix as a quick
fix:
main.pep:2:18: error: expected ';'
2 | var total = 1
| ^
| ;
Because line breaks do not end anything, a statement missing its ; would
otherwise run on into the next line whenever that line can continue it:
x = y
-z
reads as x = y - z; with its ; missing, not as two statements. The error
says where the ; belongs when the parse cannot go on.
Control flow
Pepper has the familiar structured control flow of C-family languages: if and
else to choose, while and for to repeat, and break, continue and
return to leave early.
if and else
if runs a block when its condition is true, and the optional else block
when it is false. The parentheses around the condition are optional; the braces
around each block are required, even for a single statement.
# Branching with if and else.
classify(n: int): String {
if n < 0 {
return "negative";
} else if n == 0 {
return "zero";
} else {
return "positive";
}
}
main(): int {
var temperature = 25;
# Parentheses around the condition are optional; braces are required.
if temperature > 30 {
print("hot");
} else {
print("not hot");
}
if (temperature > 20) {
temperature = 20;
}
assert(temperature == 20, "clamped to 20");
assert(classify(-5) == "negative", "negative");
assert(classify(0) == "zero", "zero");
assert(classify(7) == "positive", "positive");
# For a choice between two values, the ternary operator is shorter.
var parity = temperature % 2 == 0 ? "even" : "odd";
assert(parity == "even", "20 is even");
# && and || stop at the first operand that decides the result, so the
# count check guards the index.
var readings = Array<int>();
var starts_high = false;
if (readings.count > 0 && readings[0] > 100) {
starts_high = true;
}
assert(!starts_high, "an empty array does not start high");
return 0;
}
The condition is usually a bool, but a number also works: any value other
than zero counts as true, as in C. A raw pointer (__ptr<T>) or a function
value counts as true when it is not null. A struct counts only if it declares
the truth-test operator !!(): bool, which the condition calls; any other
struct is an error, so write node !== __null to ask whether a reference is
set (see Truth tests and conditions).
The same holds for the condition of while and of ?:, the operands of &&,
||, and and or, and the operand of !.
Chains of conditions
else if tests another condition when the ones before it were false. The
first true condition’s block runs, and the final else block, if there is
one, runs when none was true:
if x > 0 {
return 1;
} else if x < 0 {
return -1;
} else {
return 0;
}
For a choice between two values, the conditional operator
condition ? a : b is often shorter than an if.
Guarding a check
&& and || short-circuit, as in C: the right operand is evaluated only when
the left one does not already decide the result. A guard can therefore come
first in the same condition:
if items.count > 0 && items[0] > 100 {
print("starts high");
}
When items is empty, items[0] is never read.
while
while repeats its block as long as the condition is true. The condition is
checked before each iteration, so the body may run zero times. As with if,
the parentheses are optional and the braces are required.
var countdown = 3;
while countdown > 0 {
countdown -= 1;
}
for
for runs its block once for each element of an Array,
each code point of a String, or each value of any
type that follows the iterator protocol:
for (var score in scores) {
total += score;
}
The loop variable can be declared with var or a type
(for (int score in scores), for (var score: int in scores)), and the
parentheses are optional. Iteration covers the protocol behind
for and how to make your own types iterable.
Pepper has no C-style for (init; condition; step) loop. To count, loop over
a range: for (var i in 0..count).
with
with enters a scope object for the length of a block and binds what it
hands out. The object cleans up when it is destroyed, on every way out of the
block:
with (var session = Session("grace")) {
greet(session.user);
}
Scoped blocks with with covers the protocol and the optional
else block.
match
match runs the arm for the case an enum value holds, and the
compiler checks that every case is handled:
match (light) {
Red, Amber => { stop(); }
Green => { go(); }
}
Enums and match covers patterns and the else arm.
Branch hints: [Likely] and [Unlikely]
[Likely] or [Unlikely] on the line above an if, an else if’s if
or a match arm (the else arm too) says how often that branch runs. The
optimizer keeps the likely path as straight-line code and moves the
unlikely one, such as error handling, out of its way. A hint never changes
what the program computes; a wrong one costs only speed.
# Branch hints: `[Likely]` and `[Unlikely]` say which way a branch usually
# goes, so the optimizer keeps the common path straight.
enum Reading {
Value(celsius: double),
Missing,
}
parse_digit(c: char): int {
[Unlikely]
if (c < '0' || c > '9') {
return -1;
} else [Likely] if (c != '0') {
return c - '0';
}
return 0;
}
describe(reading: Reading): String {
match (reading) {
[Likely]
Value(celsius) => {
return $"{celsius}";
}
[Unlikely]
Missing => {
return "missing";
}
}
}
main(): int {
assert(parse_digit('7') == 7, "a digit");
assert(parse_digit('x') == -1, "not a digit");
assert(describe(Reading.Missing) == "missing", "the rare arm still runs");
return 0;
}
-
[Likely]above anifmarks its block, so itselseis the unlikely side;[Unlikely]marks the block as the rare one. To mark anelse, mark theifthe other way. -
In an
else if, the attribute goes betweenelseandif:} else [Unlikely] if (x) {. -
A hint is an LLVM branch weight (2000 to 1, as clang gives
__builtin_expect), so it matters with optimization (-O1and up). -
Only one of them, with no arguments, can mark a branch, and no other attribute can:
main.pep:3:5: error: 'Target' cannot mark an 'if'; only '[Likely]' or '[Unlikely]' can 3 | [Target(OS(Linux))] | ^~~~~~~~~~~~~~~~~~~
while loops and the arms of a match expression take no hint yet.
break and continue
break leaves the innermost loop immediately. continue skips the rest of the
current iteration and goes on with the next one; in a while loop that means
checking the condition again, so update the counter before any continue.
# Looping with while and for, and leaving early with break and continue.
main(): int {
# while repeats as long as its condition is true.
var countdown = 3;
var launched = false;
while countdown > 0 {
countdown -= 1;
}
launched = countdown == 0;
assert(launched, "counted down to zero");
# for visits each element of an Array (or any other iterable).
var scores = Array<int>();
scores.append(70);
scores.append(85);
scores.append(92);
var total = 0;
for (var score in scores) {
total += score;
}
assert(total == 247, "sum of the scores");
# continue skips to the next iteration; break leaves the loop.
var odd_sum = 0;
var i = 0;
while (i < 100) {
i += 1;
if (i % 2 == 0) { continue; }
if (i > 9) { break; }
odd_sum += i;
}
assert(odd_sum == 25, "1 + 3 + 5 + 7 + 9");
# break and continue apply to the innermost loop.
var pairs = 0;
for (var a in scores) {
for (var b in scores) {
if (b == a) { break; }
pairs += 1;
}
}
assert(pairs == 3, "(85, 70), (92, 70), (92, 85)");
return 0;
}
Both work in while and for loops and always apply to the innermost loop.
There are no labeled breaks: to leave several loops at once, set a flag and test
it, or move the loops into a function and return from it.
return
return value; leaves the current function and hands value back to the
caller. It works from anywhere in the function, including from inside loops.
See Functions.
In a function that returns nothing, a bare return; leaves the function early.
Functions
A function is a named block of code that takes parameters and can return a value. Functions declared at the top level of a file are free functions; functions declared inside a struct are methods, covered in Structs.
Declaring a function
Write the name, the parameters in parentheses as name: Type, and the return
type after a colon:
area(width: int, height: int): int {
return width * height;
}
Pepper also accepts a C-style form, with the return type first and each parameter’s type before its name:
int perimeter(int width, int height) {
return 2 * (width + height);
}
Both forms declare the same kind of function; pick one style and use it consistently. The first form is the one used throughout this book.
# Declaring and calling functions.
import async;
# Parameters are written 'name: Type'; the return type follows the ')'.
area(width: int, height: int): int {
return width * height;
}
# The C-style form puts the return type first and the type before each name.
int perimeter(int width, int height) {
return 2 * (width + height);
}
# A function that returns nothing leaves out the return type
# (or writes ': void', or 'void' in front).
# A module-scope counter: const, and changed through an Atomic.
const log_count = async.Atomic<int>(0);
log(message: String) {
log_count.fetch_add(1);
print("log: " + message);
}
# Functions can call themselves, and can be declared in any order.
factorial(n: int): long {
if (n <= 1) { return 1; }
return n * factorial(n - 1);
}
main(): int {
assert(area(3, 4) == 12, "3 x 4");
assert(perimeter(3, 4) == 14, "2 x (3 + 4)");
assert(factorial(20) == 2432902008176640000, "20!");
assert(is_even(10), "declared after main, still callable");
# An argument converts to its parameter's type as assignment does:
# a double passed to an int parameter truncates toward zero.
assert(area(2.9, 3) == 6, "2.9 becomes 2");
log("starting");
log("done");
assert(log_count.load() == 2, "logged twice");
return 0;
}
is_even(n: int): bool {
return n % 2 == 0;
}
-
Order does not matter. A function can call functions declared later in the file, and can call itself.
-
The program starts at
main.main(): intreturns the process’s exit code; return 0 for success. To receive the command-line arguments, declare itmain(args: String[]): intinstead. See Command-line arguments and exit. -
Calls must match the declaration. Passing the wrong number of arguments is a compile error:
main.pep:3:12: error: too many arguments to 'add': expected 2, have 3 3 | return add(1, 2, 3); | ^~~
Functions that return nothing
A function that returns no value leaves out the return type. You can also write
: void, or void in front in the C-style form:
log(message: String) { print(message); }
log_line(message: String): void { print(message); }
void log_c(String message) { print(message); }
Such a function ends when it reaches the closing brace, or earlier at a bare
return;:
log_positive(n: int) {
if n <= 0 {
return;
}
print($"{n}");
}
A call to such a function has no value, so it cannot be stored, passed,
returned, or used as an operand or condition: var x = log("hi"); reports
cannot use a value of type 'void' as variable 'x'.
In a function that returns a value, return; is an error:
main.pep:2:5: error: non-void function 'f' should return a value
2 | return;
| ^~~~~~
Inferred return types
A function without a return type gets one from its return statements. It
returns void when no return has a value, and otherwise the type of the first
return that has one, so half(x: double) { return x / 2; } returns a
double. A generic function’s return type is inferred for each
specialization, so first<T>(a: T, b: T) { return a; } returns an int for
first<int>. main with no return type returns an int, and reaching its
closing brace exits with 0.
Writing the return type is still clearer for a function that returns a value,
and it lets the compiler check each return against it.
Parameters
Parameters are passed by value. For numbers and bool, the function receives
its own copy. For a String, an Array or any other struct, the value is a
reference, so the function receives another reference to the same object: it
can change the object’s contents, and the caller sees the change. Assigning a
new object to the parameter does not affect the caller’s variable.
Memory and reference counting describes exactly how references are
counted when you pass them.
Arguments convert to the parameter’s type the same way assignment does, so you
can pass an int where a long or double is expected, a double where an
int is expected (it truncates toward zero, like int n = 2.7;), and a
String where a struct with an
[Implicit] constructor from String (such
as io.Path) is expected.
Default parameter values
A parameter can have a default value, written = value after its type. A call
that leaves the argument out gets the default. Put parameters with defaults at
the end of the list, since arguments are matched from the left.
# Default parameter values.
# Parameters with defaults come last; a call may leave them out.
greet(name: String, greeting: String = "Hello", punctuation: String = "!"): String {
return greeting + ", " + name + punctuation;
}
# Defaults work with the C-style form too.
int clamp(int value, int low = 0, int high = 100) {
if (value < low) { return low; }
if (value > high) { return high; }
return value;
}
main(): int {
assert(greet("Ada") == "Hello, Ada!", "both defaults");
assert(greet("Ada", "Hi") == "Hi, Ada!", "one default");
assert(greet("Ada", "Hi", ".") == "Hi, Ada.", "no defaults");
assert(clamp(150) == 100, "clamped to the default high");
assert(clamp(-5) == 0, "clamped to the default low");
assert(clamp(-5, -10) == -5, "low given");
return 0;
}
Defaults work the same way for methods and constructors. To give a later parameter a value without passing every parameter before it, name it (see Named arguments).
Named arguments
A call can name its arguments, name = value, so it reads clearly and can
set one defaulted parameter without passing the ones before it:
# Named arguments: `name = value` after the positional arguments.
window(title: String, width: int = 800, height: int = 600, resizable: bool = true): String {
return $"{title} {width}x{height} resizable={resizable}";
}
struct Point {
var x: int;
var y: int;
construct(this.x = 0, this.y = 0) {}
moved(dx: int = 0, dy: int = 0): Point {
return Point(x + dx, y + dy);
}
}
# 'size = ...' works as well as 'count = ...'; 'n = ...' still works, with a
# deprecation warning.
[Alias("size", "count")]
[Rename("n", "count")]
repeat(text: String, count: int = 1): String {
var result = "";
for (var i in 0..count) {
result = result + text;
}
return result;
}
main(): int {
assert(window("Editor") == "Editor 800x600 resizable=true", "all defaults");
# Skip width and height.
assert(window("Editor", resizable = false) == "Editor 800x600 resizable=false", "skip");
# Named arguments in any order.
assert(window("Editor", height = 400, width = 1200) == "Editor 1200x400 resizable=true", "any order");
# Every argument may be named.
assert(window(title = "Editor", width = 1024) == "Editor 1024x600 resizable=true", "all named");
# Constructors and methods take them too.
var p = Point(y = 5);
assert(p.moved(dy = 2).y == 7, "method");
assert(repeat("ab", size = 3) == "ababab", "alias");
return 0;
}
- Positional first. Positional arguments come first and match parameters
from the left; named arguments follow, in any order. A positional argument
after a named one is an error:
positional argument cannot follow a named argument. - Each parameter once. Naming a parameter that already has an argument is
an error:
argument for 'width' given twice. - Every required parameter. A parameter without a default must get an
argument:
missing argument for parameter 'title' in call to 'window'. A name the function does not have is an error, with a suggestion when one is close:no parameter named 'widht' in 'window'; did you mean 'width'?. - Order of evaluation. Arguments are evaluated in the order written, then passed to their parameters.
- Everywhere a call is. Free functions, methods, static methods,
constructors (a
this.xparameter is namedx), extension methods, enum methods, generic functions, and calls through a trait value, which use the trait’s parameter names. A method that implements a trait should use the trait’s names, so a call names the same arguments either way: a different name is a warning (parameter 'factor' of 'area' is named 'scale' in trait 'Shape') whose fix renames the parameter. - Overloads. A member of an overload set without a parameter of a given name is not a candidate, so names can pick between overloads.
- Not through function values. A function type
carries no parameter names, so a call through a function value is
positional only:
cannot name arguments in a call through a function value of type '(int, int) => int'. - Not to
externfunctions. Their parameter names are copied from C headers and stay free to change:cannot name arguments in a call to extern function 'CreateFileW'. Wrap the C function in a Pepper function to give it named arguments.
Other names for a parameter
Two attributes on a function give its parameters more names. In both, the declared parameter’s name comes second:
[Alias("size", "count")]lets a call name parametercountassizetoo, permanently.[Rename("n", "count")]keeps the former namenworking after the parameter was renamed tocount. Each use warns, like a deprecated declaration, with a fix that renames it.
The first name must not already be a parameter or another name, and the
second must be a parameter of the function. A call that names one parameter
by two of its names gets argument for 'count' given twice.
Returning several values
A function returns several values as a tuple, and the caller destructures it:
divide(a: int, b: int): (int, int) {
return (a / b, a % b);
}
var (quotient, remainder) = divide(17, 5);
Overloading
Functions can share a name when their parameter lists differ, in one file, across a module’s files, or across the modules a file imports, and each call picks the best fit for its arguments. See Overloading.
Generic functions
A function can take type parameters in angle brackets after its name, such as
first<T>(items: T[]): T. Generics covers them.
Printing with print
print(value) writes a value’s text and a line break to standard output:
what interpolating it,
$"{value}", would make. That covers a String, numbers, bool (true or
false), tuples, and any type with a to_string(). To print several values
on one line, interpolate them (print($"total: {n}")), or pass a format
string and its arguments, which prints what
String.format makes
of them (print("{0} of {1}", done, total)).
# print writes a value's text and a line break.
main(): int {
print("Hello, world!");
print(42); # any value with a text form
print((1.5, true)); # (1.5, true)
var name = "Pepper";
var count = 42;
print($"{name} has {count} items"); # interpolate other values
print("{0} + {1} = {2}", 3, 4, 3 + 4); # or format them: "3 + 4 = 7"
print("{braces} alone print as written");
return 0;
}
- With a string literal and no other arguments, the text prints as written: its braces are not a format.
- A value with no text form is an error at the argument:
main.pep:5:11: error: cannot print a value of type 'Foo': it has no 'to_string()' method and no 'to_string' overload
5 | print(f);
| ^
print writes each line out as it prints it, like Rust’s println!, so the
output appears as the program runs even when standard output is a pipe or a
file. Text from print and from io.File.stdout() comes out in the order
the program wrote it.
A float or double prints its shortest exact text (print(0.5) prints
0.5), and a tuple its elements in parentheses ((1, two)).
Assertions and panics
For checks in your programs, the runtime provides assert(condition, message), which stops the program with message when condition is false, and
panic(message), which always stops it. Each is
overloaded: the message may be a String, built at run time
(assert(n > 0, name + " must be positive")), or a C string, which a string
literal picks, so a literal message builds nothing when the assertion holds. See
Built-in functions.
A panic flushes what the program printed, writes the location of the failing call and the message to standard error, and aborts:
main.pep:12:5: panic: count must be positive
In a build without -O, a backtrace of the calls that led there follows the
message. PEPPER_BACKTRACE=1 turns it on in an optimized build and
PEPPER_BACKTRACE=0 turns it off (see
Backtraces and the debugger).
The checks the compiler adds report the same way, at the expression that
failed: an overflowing operator, a
null function value, a match on a null reference enum. So do the runtime’s
checking methods, which report their caller’s line, not their own: indexing an
Array or String out of range, Array.pop() on an empty array,
Optional.value() on an Empty optional, Result.value() on an error, and the like.
They do it with an ordinary default parameter,
location: __ptr<char> = __caller_location(). A call that leaves location
out gets the place of that call, as file:line:col. Write the same parameter on
a function of your own that checks its arguments, and pass location on to
assert or panic, so a failure names the line that passed the bad value:
# Reporting the caller's location with `__caller_location()`.
# A check that names its caller's line when it fails, the way `assert`,
# `panic`, `Array` indexing and `Optional.value()` do: the default is the
# location of the call that leaves `location` out.
check_positive(value: int, location: __ptr<char> = __caller_location()): int {
assert(value > 0, "expected a positive value", location);
return value;
}
# The location is a C string, `file:line:col`.
here(location: __ptr<char> = __caller_location()): String {
return String(location);
}
main(): int {
assert(check_positive(3) == 3, "positive");
var at = here();
assert(at.ends_with("functions-caller-location.pep:18:14"), "this call");
return 0;
}
Written anywhere else, __caller_location() is its own location. A
compiler-generated check with no single place to report, such as a call through
a null trait value, prints the message alone.
Command-line arguments and exit
main comes in two forms. main(): int takes nothing. main(args: String[]): int receives the program’s command-line arguments: element 0 is
the program as it was started, and the arguments typed after it follow in
order. Declare the parameter only when the program reads it, as a Windows
console program declares argc/argv only when it needs them. Any other
parameter list on main is an error, and so is declaring both forms.
pepper build --entry <name> (and pepper run --entry <name>) starts the
program in the top-level function name instead, which takes either form
of parameter list, as main does. Every main is then left out of the
program, so a file can hold several entry points, such as a benchmark
beside the tool it measures.
Elsewhere, process.arguments() returns the same array, so code outside
main can read the arguments without having them passed down.
process.exit(code) ends the program at once with status code, from
anywhere, after flushing what was printed. Both are in the
process module, so the file needs import process.
import process;
# args holds the program name, then each command-line argument.
main(args: String[]): int {
if (args.count > 2) {
print("usage: greet [NAME]");
process.exit(2); # ends the program here, with status 2
}
var name = "world";
if (args.count == 2) {
name = args[1];
}
print("Hello, " + name + "!");
return 0;
}
exit skips everything that would run on the way out of main: locals are
not released and destructors do not run. Prefer returning from main, and
keep exit for ending from deep inside a call. See
Processes.
Functions as values
A function’s name used without a call is a value that you can store in a
variable, pass to another function, or return, and a closure such as
(x: int) => x * 2 is a function written where it is used. See
Function types and closures.
Tuples and multiple return values
A tuple groups a fixed number of values, each of its own type, into one value. Its main use is returning several values from a function:
# A function returns several values as a tuple.
divide(a: int, b: int): (int, int) {
return (a / b, a % b);
}
# Elements can be of any type, and a tuple can hold another tuple.
describe(n: int): (String, (bool, int)) {
return ("number", (n % 2 == 0, n * n));
}
main(): int {
# Read elements by position: t.0, t.1, ...
var t = divide(17, 5);
assert(t.0 == 3, "the quotient");
assert(t.1 == 2, "the remainder");
# Nested tuples chain: d.1.0 is element 0 of element 1.
var d = describe(4);
assert(d.0 == "number", "the name");
assert(d.1.0 && d.1.1 == 16, "even, and its square");
# A tuple type can be written out; the literal converts to it.
var wide: (long, double) = (1, 2);
assert(wide.0 == 1 && wide.1 == 2.0, "converted elements");
return 0;
}
Tuple types and values
- A tuple type lists its element types in parentheses:
(int, String),(bool, (int, int)). A tuple has at least two elements. - A tuple value lists its elements the same way:
(a / b, a % b). The compiler infers its type from the elements, so(1, "x")is an(int, String). - Where a tuple type is expected (a declared variable, a parameter, a return
value), each element converts to its element type, as it would on its own:
var wide: (long, double) = (1, 2);. t.0,t.1, … read an element by position, counting from 0.t.1.0is element 0 of element 1. An element that does not exist is a compile error.- Elements are read-only.
t.0 = 3is a compile error; build a new tuple instead:t = (3, t.1);. Tuple<A, B>is another spelling of(A, B), for places where a type in angle brackets reads better.(int, String)?is an optional tuple,Optional<(int, String)>(see Optional values).
Labels
A tuple type can name its elements: (line: int, column: int), and so can a
tuple value: (line: 3, column: 7). A label is another name for a position,
so p.line and p.0 read the same element:
# Labels name a tuple's elements.
position(): (line: int, column: int) {
return (line: 3, column: 7);
}
struct Token {
var text: String = "";
var at: (line: int, column: int);
}
main(): int {
var p = position();
assert(p.line == 3 && p.column == 7, "read by label");
assert(p.0 == 3, "positions still work");
# Labeled and unlabeled tuples convert to each other.
var plain: (int, int) = p;
assert(plain.1 == 7, "same elements");
# A labeled value carries its labels into the variable's type.
var end = (line: 4, column: 1);
assert(end.line == p.line + 1, "labeled literal");
var t = Token();
t.at = (5, 6);
assert(t.at.column == 6, "a labeled field");
return 0;
}
- Labels belong to the type you write, not to the value. A labeled tuple and an unlabeled one with the same element types are the same type, so each converts to the other freely.
- Either every element has a label or none does, and labels in one tuple must differ.
- Labels do not pass through a type argument:
Optional<(line: int, column: int)>is the same type asOptional<(int, int)>, so itsvalue()returns an unlabeled tuple. Declare a labeled variable to name the elements again. var p = (line: 3, column: 7);givespthe labeled type(line: int, column: int). A labeled value takes at least two elements.- A labeled value initializing or returned as a labeled type must use its
labels in order:
(column: 7, line: 3)where a(line: int, column: int)is expected is an error, not a swap. An unlabeled value, or a labeled one going to an unlabeled type, converts by position. - A destructuring assignment lists places alone:
(a, b) = (b, a);, never(x: a, y: b) = ....
Destructuring
var (a, b) = value; declares one variable for each element of a tuple:
struct Point {
var x: int = 0;
var y: int = 0;
}
min_max(values: Array<int>): (int, int) {
var low = values[0];
var high = values[0];
for (var v in values) {
if (v < low) {
low = v;
}
if (v > high) {
high = v;
}
}
return (low, high);
}
main(): int {
var values = Array<int>();
values.append(4);
values.append(-2);
values.append(9);
# Destructure the result into new variables.
var (low, high) = min_max(values);
assert(low == -2 && high == 9, "both ends");
# `_` skips an element; a binding may state its type.
var (_, top: long) = min_max(values);
assert(top == 9, "only the maximum");
# `const` makes every binding constant.
const (first, last) = (values[0], values[2]);
assert(first == 4 && last == 9, "constants");
# Assign to existing places. The right side is evaluated first,
# so this swaps.
var a = 1;
var b = 2;
(a, b) = (b, a);
assert(a == 2 && b == 1, "swapped");
var p = Point();
(p.x, p.y) = min_max(values);
assert(p.x == -2 && p.y == 9, "fields are places too");
return 0;
}
- The number of names must match the number of elements.
_skips an element.- A name may state its type,
var (_, top: long) = …; the element converts to it. Without one, the variable takes the element’s type. const (a, b) = …declares constants.- Patterns nest:
var (id, (name, age)) = …takes an element that is itself a tuple apart. - A
forloop destructures each element:for (var (name, count) in pairs). (a, b) = value;assigns to places that already exist: variables, fields (p.x) and array elements. The whole right side is evaluated before any assignment, so(a, b) = (b, a)swaps.
Tuples and references
A tuple is a value, like a number. It is not an object on the heap, and copying it copies its elements. An element that is a reference is shared, not copied, exactly as if you had assigned it to a variable:
- Building a tuple from a variable adds a reference to the object; building it from a function result takes over that result’s reference.
- A tuple holds its references until it goes out of scope or is replaced, then releases them.
- Destructuring moves each element into its variable. After
var (a, b) = pair();,aholds the only reference to its object, just as it would aftervar a = make();.
A tuple is thread-safe when all its elements are, so a module-scope
const origin: (int, long) = (0, 0); is allowed.
Collections, comparing and printing
A tuple works anywhere a type does, including as a type argument:
Array<(int, String)>, HashMap<(int, int), String>. Iterating a HashMap
yields (key, value) tuples:
main(): int {
# A tuple can be a key: here, a grid position.
var grid = HashMap<(int, int), String>();
grid[(0, 0)] = "start";
grid[(2, 3)] = "goal";
assert(grid[(2, 3)] == "goal", "looked up by value");
# Tuples compare element by element.
assert((1, "a") == (1, "a"), "equal");
assert((1, "a") != (1, "b"), "different");
# A HashMap yields (key, value) tuples; destructure them in the loop.
var ages = HashMap<String, int>();
ages["ada"] = 36;
ages["grace"] = 85;
var total = 0;
for (var (name, age) in ages) {
total += age;
}
assert(total == 121, "every entry");
# An array of tuples.
var pairs = Array<(String, int)>();
pairs.append(("x", 1));
pairs.append(("y", 2));
for (var (name, count) in pairs) {
print($"{name}: {count}");
}
return 0;
}
==and!=compare element by element. Each element compares the way it would on its own: a type with an==operator (such asString) uses it, and any other struct compares by identity (the same object).- A tuple can be a
HashMapkey or aHashSetmember. Its hash combines the hashes of its elements, so equal tuples hash equally. to_string((1, "two"))is(1, two), so a tuple of up to eight values that each have ato_stringcan be interpolated and printed:print($"{t}").copy(t)(see Copying a value) copies each element: a struct element ascopywould copy it on its own, anything thread-safe as is.
Matching tuples
A match over a tuple tests all of its elements at once.
A tuple pattern lists a pattern for each element: (Rock, Scissors),
(Number(v), 1), (4, column). Write the tuple in the match itself with
double parentheses, match ((a, b)), or match a tuple value, match (at).
# `match` takes a tuple apart, testing every element at once.
enum Move {
Rock,
Paper,
Scissors,
}
# Who wins: 1 for the first player, 2 for the second, 0 for a draw.
winner(first: Move, second: Move): int {
match ((first, second)) {
(Rock, Scissors), (Paper, Rock), (Scissors, Paper) => {
return 1;
}
(Rock, Rock), (Paper, Paper), (Scissors, Scissors) => {
return 0;
}
else => {
return 2;
}
}
}
enum Token {
Number(value: int),
Word(text: String),
}
# Literals, cases with values, bound names and `_` mix in one pattern.
describe(token: Token, line: int): String {
return match ((token, line)) {
(Number(v), 1) => "a number on the first line",
(Word(text), _) => text,
(_, _) => "another number",
};
}
main(): int {
assert(winner(Move.Rock, Move.Scissors) == 1, "rock blunts scissors");
assert(winner(Move.Paper, Move.Scissors) == 2, "scissors cut paper");
assert(winner(Move.Paper, Move.Paper) == 0, "a draw");
assert(describe(Token.Word("hi"), 3) == "hi", "a word binds its text");
assert(describe(Token.Number(7), 1) == "a number on the first line", "a literal element");
# A name binds its element; `is` tests one pattern.
var at = (4, 2);
if (at is (4, column)) {
assert(column == 2, "bound by the pattern");
}
return 0;
}
- Each element takes a case of its enum (
Rock,Word(text)), a literal (1,"x",true), a range or a constant (0..9,LIMIT), another tuple pattern,_for any value, or a name. - A name binds its element, unless it names a module-scope constant, which
the element is compared with, or the element is an enum with a case of
that name, which it then matches. So in
(Circle, other)over two shapes,Circleis a case andotherholds the second shape. - An arm’s patterns bind the same names, each at the same element:
(x, 0), (0, x)is an error, sincexwould mean a different element depending on which pattern matched. - The arms must handle every value unless there is an
else. A missing value is named with an example:match on '(Shape, int)' does not handle '(Line, _)'. Only enum cases andtrue/falsecan be listed in full; other literals and ranges need a pattern with_or a name at that element. - A pattern that the arms before it already cover is an error, and an
elseafter arms that cover everything is a warning. value is (4, column)tests one tuple pattern, and binds its names as the whole condition of aniforwhile, as for enums.
Collection literals
A collection literal writes an array, a map or a set in one expression:
trait Shape {
area(): int;
}
struct Square {
int side;
construct(side: int) {
this.side = side;
}
}
extend Square with Shape {
area(): int {
return side * side;
}
}
total(values: long[]): long {
long sum = 0;
for (var value in values) {
sum += value;
}
return sum;
}
main(): int {
# An array literal is an Array of its elements' type.
var primes = [2, 3, 5, 7];
assert(primes.count == 4 && primes[3] == 7, "an Array<int>");
# A trailing comma lets a long literal put one element per line.
var operators = [
"===",
"!==",
"=>",
];
assert(operators[1] == "!==", "an Array<String>");
# Where an Array type is expected, the elements convert to its element type.
assert(total([1, 2, 3]) == 6, "an Array<long> argument");
Shape[] shapes = [Square(2), Square(3)];
assert(shapes[1].area() == 9, "elements convert to the trait type");
# A map literal is a HashMap; a repeated key keeps its last value.
var ages = ["Ada": 36, "Alan": 41];
assert(ages["Alan"] == 41, "a HashMap<String, int>");
# An array literal where a HashSet is expected makes a set.
HashSet<String> keywords = ["if", "else", "if"];
assert(keywords.count == 2, "duplicates are kept once");
# [] and [:] are empty, where the type is declared.
long[] none = [];
HashMap<String, int> nobody = [:];
assert(none.is_empty && nobody.count == 0, "empty collections");
return 0;
}
Arrays
[a, b, c]is a newArray<T>holding the elements in order. A trailing comma is allowed:[a, b, c,].- With no other type to go by,
Tis the first element’s type, and every later element converts to it the way an argument converts to a parameter:[1, 2]is anArray<int>, and[1, "a"]is an error. - Where an array type is expected (a declared variable, a parameter, a
return value, an assignment),
Tis that type’s element type instead, and every element converts to it:long[] a = [1, 2];,Shape[] s = [square, circle];. - Nested literals are typed the same way:
long[][] grid = [[1], [2, 3]];. []is an empty array. It needs a declared type (long[] none = [];), since it has no element to take a type from:
main.pep:2:13: error: empty array literal has no element type; declare the variable's type or write 'Array<T>()'
2 | var a = [];
| ^~
Maps
[k1: v1, k2: v2]is a newHashMap<K, V>holding the entries.KandVcome from the first entry, or from theHashMap<K, V>a declared variable, parameter or return type expects.- A key written twice keeps its last value, as two
insertcalls would. [:]is an empty map, and needs a declared type:HashMap<String, int> none = [:];.
Sets
There is no separate set literal. An array literal where a
HashSet<K> or a
Set<K> is expected converts to one through
the set’s implicit constructor,
which keeps each element once: HashSet<String> keywords = ["if", "else"];.
Types of specializations
A literal never changes a collection’s type after it is built:
Array<int> and Array<long> are different types, and neither converts to
the other.
main.pep:2:21: error: cannot initialize variable 'a' of type 'Array<long>' with a value of type 'Array<int>'
2 | Array<long> a = Array<int>();
| ^~~~~~~~~~
Arrays and slices
Array<T> is Pepper’s growable list. Beyond
append, indexing and sort, it has the everyday algorithms, and
slice gives a function “some of the elements” without copying them:
total(values: ArraySlice<int>): int {
var sum = 0;
for (var x in values) {
sum = sum + x;
}
return sum;
}
main(): int {
# Searching a sorted array.
var sorted = [1, 3, 3, 5, 8];
assert(sorted.is_sorted(), "sorted");
assert(sorted.binary_search(5).value() == 3, "found at 3");
assert(sorted.binary_search(4).is_empty(), "no 4");
assert(sorted.upper_bound(3) - sorted.lower_bound(3) == 2, "two 3s");
# Changing elements in place.
var a = [4, 4, 1, 2, 2, 7];
a.dedup();
assert(a == [4, 1, 2, 7], "adjacent repeats removed");
a.retain((x) => x % 2 == 0);
assert(a == [4, 2], "odd elements removed");
a.resize(4, 0);
assert(a == [4, 2, 0, 0], "padded with 0");
var tail = a.split_off(2);
assert(a == [4, 2] && tail == [0, 0], "split in two");
tail.fill(9);
assert(tail == [9, 9], "filled");
# A slice views part of an array without copying it.
var numbers = [1, 2, 3, 4, 5];
var middle = numbers.slice(1, 4);
assert(middle.count == 3 && middle[0] == 2, "views [2, 3, 4]");
assert(total(middle) == 9, "passed to a function");
assert(middle.to_array() == [2, 3, 4], "copied out");
return 0;
}
Searching a sorted array
binary_search(item)returns the index of the first element equal toitemas aulong?, in O(log count). The element type must beComparable(every number,charandStringis), and the array sorted in itscompareorder.binary_search_by(order)searches with a function that says whether an element sorts before (negative), at (0) or after (positive) the target.lower_bound(item)andupper_bound(item)are the first and last placesitemcould be inserted to keep the array sorted; the elements between them equalitem.is_sorted()reports whether the array is in that order.
Changing elements in place
fill(value)sets every element tovalue.retain(keep)removes each elementkeepreturns false for, keeping the order of the rest.keepmust not change the array; if it does,retainpanics withArray modified during retain.dedup()removes each element equal (==) to the one before it; on a sorted array that removes every duplicate.resize(count, value)shortens the array, or appends copies ofvalue.split_off(index)moves the elements fromindexon into a new array.
Each counts as modifying the array, so a for loop over it panics on its
next step.
Slices
a.slice(start, end) is an ArraySlice<T>: a read-only view of the
elements from start up to but not including end. It has count,
is_empty, indexing, for loops, slice, to_array, find, contains
and the searches above.
A slice is safe by construction:
- It holds a reference to its array, so the array lives as long as the slice does.
- It is fail-fast, like an iterator. Once the array changes (any store,
append,remove_at,sort, …), every later use of the slice panics withArray modified while a slice of it is in use, so it can never read an element that moved or a buffer that was freed. Make a new slice after changing the array.
Not yet
- Slices are read-only; there is no slice that writes through to its
array, and no
a[1..4]syntax: writea.slice(1, 4). sorttakes a comparer (a.sort((x, y) => x.compare(y))); asort()with none,sort_unstable,rotate,drain,chunksandwindowsare not there yet.Stringhas no slice type;substrcopies.
Function types and closures
Functions in Pepper are values. You can store a function in a variable, a field or an array, pass it to another function, and return it. This lets you choose behavior at run time: a sort order, a callback, a step in a pipeline. A function value is a named function, a method, a constructor, or a closure: an anonymous function written where it is used.
Function types
A function type is written (parameter types) => return type:
| Type | Meaning |
|---|---|
(int) => int | takes an int, returns an int |
(String, int) => bool | takes a String and an int, returns a bool |
() => void | takes nothing, returns nothing |
(int) => (int) => int | takes an int, returns an (int) => int |
=> is right-associative, so the last row is a function that returns a
function. A function type carries no parameter names, so a call through a
function value passes its arguments by position:
named arguments are an error there. A function type can be used anywhere a type can: for variables,
parameters, return types, struct fields, generic arguments such as
Array<(int) => int>, and type aliases.
Written with shared in front, as in shared () => void, a function type is
thread-safe: its values can be passed to other threads. See
Sharing function values between threads.
Referring to a named function
The name of a free function, written without a call, is a value of that function’s type. Calling a function value calls the function:
# Function types and references to named functions.
square(x: int): int { return x * x; }
negate(x: int): int { return -x; }
# A parameter can take a function: its type is '(parameter types) => return type'.
apply(f: (int) => int, x: int): int {
return f(x);
}
twice(f: (int) => int, x: int): int {
return f(f(x));
}
# Generic functions can take function values too.
apply_twice<T>(f: (T) => T, x: T): T {
return f(f(x));
}
main(): int {
# A function's name without a call is a value of its function type.
var f = square; # f: (int) => int
assert(f(3) == 9, "call through a local");
var g: (int) => int = negate; # the type can be written out
assert(g(4) == -4, "explicit function type");
# Function variables can be reassigned to another function of the same type.
f = negate;
assert(f(6) == -6, "reassigned");
assert(apply(square, 5) == 25, "pass a function as an argument");
assert(twice(square, 3) == 81, "(3 * 3) * (3 * 3)");
assert(twice(negate, 7) == 7, "negating twice");
assert(apply_twice<int>(square, 2) == 16, "generic");
return 0;
}
var f = square;infersf’s type,(int) => int, from the function. An overloaded name, or an overloaded method (p.move), needs a declared type instead, which picks the member.- An
externfunction is a value too, of the type its C prototype gives:var f = abs;is an(int) => int. A variadic one, such assnprintf, is not. - A function-typed variable can be reassigned to any function of the same type.
- A parameter of function type, such as
f: (int) => int, lets the caller pass in the behavior. Generic functions can take function parameters too.
Methods, constructors and generic functions
Methods, constructors and specializations of generic functions are values too:
# Methods, constructors and generic functions used as values.
struct Point {
int x;
int y;
construct(this.x = 0, this.y = 0) {}
norm(): int {
return x + y;
}
shifted(dx: int): Point {
return Point(x + dx, y);
}
# Inside a method, a bare method name is `this.norm`.
norm_of_this(): () => int {
return norm;
}
}
struct Box<T> {
T value;
construct(v: T) {
value = v;
}
map<U>(f: (T) => U): Box<U> {
return Box<U>(f(value));
}
}
larger<T>(a: T, b: T): T {
return a > b ? a : b;
}
combine(f: (int, int) => int, a: int, b: int): int {
return f(a, b);
}
main(): int {
var p = Point(3, 4);
# A bound method: the method of one object, which it keeps alive.
var norm = p.norm;
assert(norm() == 7, "bound method");
assert(p.norm_of_this()() == 7, "bare method name");
# An unbound method takes the object as its first argument.
var norm_of: (Point) => int = Point.norm;
assert(norm_of(Point(1, 1)) == 2, "unbound method");
# A struct's name is its constructor.
var make: (int, int) => Point = Point;
assert(make(5, 6).y == 6, "constructor");
# A generic function with its type arguments names one specialization.
assert(combine(larger<int>, 2, 9) == 9, "generic function");
# Where a function type is expected, it picks the specialization.
assert(combine(larger, 4, 1) == 4, "inferred specialization");
# A generic method, with its type arguments written or picked by the
# function type.
var b = Box<int>(3);
var to_long = b.map<long>;
assert(to_long((x: int): long => x * 2).value == 6, "generic method");
var to_text: ((int) => String) => Box<String> = b.map;
assert(to_text((x: int) => "three").value == "three", "inferred generic method");
var steps = Array<(int) => Point>();
steps.append(p.shifted);
steps.append(Point(10, 0).shifted);
assert(steps[1](5).x == 15, "methods in an array");
return 0;
}
| Written | Type | Calls |
|---|---|---|
p.norm | () => int | p.norm(), on the object p referred to when the value was made |
Point.norm | (Point) => int | norm on the object passed as the first argument |
Point.origin (a static method) | () => Point | Point.origin() |
Point | (int, int) => Point | the constructor |
Box<int> | (int) => Box<int> | the constructor of Box<int> |
larger<int> | (int, int) => int | the specialization larger<int> |
b.map<long> (a generic method) | ((int) => long) => Box<long> | b.map<long>(...), on the object b referred to |
-
Bound methods.
p.normkeeps a reference to the object, like a closure() => p.norm()that capturedp, so the object lives as long as the value does. The expression before the.is evaluated once, when the value is made. A bound method is a function value, not the value the method returns: passingp.namewhere aStringis expected is an error. Call it,p.name(). The methods of an enum whose cases carry nothing are not values at all (see enums). -
Inside a method. A bare method name is a value too, as a bare call is a call:
normmeansthis.norm, and a static method’s bare name meansPoint.origin. This works in closures, extensions and trait default methods as well. A local or parameter of the same name hides the method. Storingnorm(orthis.norm) in a field ofthiswould be a reference cycle, and is an error, as for closures. -
Traits and extensions. A method declared in an extension works the same way, and so does a method of a trait-typed value (
shape.area), which calls through the trait. -
Generic functions. Where a function type is expected, the generic function’s name alone is enough: the expected type picks the specialization, as a call’s arguments do.
var f: (int, int) => int = larger;andcombine(larger, 2, 9)both meanlarger<int>, and a type parameter that appears only in the return type is bound from the expected return type. With no function type to go by, as invar f = larger;, write the type arguments:main.pep:3:13: error: cannot infer the type arguments of 'larger' used as a value; write them (larger<T>) or give the value a function type -
Generic methods. A method with type parameters of its own is a value once they are known: written (
b.map<long>,Box<int>.map<long>,Box<int>.wrap<int>) or picked by the function type it flows into, as for generic functions:var m: ((int) => String) => Box<String> = b.map;isb.map<String>. Without either,var m = b.map;is an error that asks for them. -
A type with a constructor that takes no arguments.
Box<int>on its own names the constructor only when the constructor needs at least one argument; otherwise it is a call,Box<int>(). The same goes for a generic function whose parameters all have defaults.
Storing and returning functions
Function values go wherever other values go: struct fields, arrays, module-level variables and return values. A type alias gives a long function type a short name, and can have type parameters of its own.
# Function values in fields, arrays, globals, and return values.
import async;
square(x: int): int { return x * x; }
negate(x: int): int { return -x; }
increment(x: int): int { return x + 1; }
struct Transform {
var op: (int) => int;
construct(this.op = square) {}
run(x: int): int { return op(x); }
}
# Choose a function at run time and return it.
pick(squared: bool): (int) => int {
if squared {
return square;
}
return negate;
}
# A module-level function variable. Every module-level variable is const
# and thread-safe: an Atomic lets it change, and a function type every thread
# can reach is declared shared.
const handler = async.Atomic<shared (int) => int>(increment);
# A type alias can name a function type, even a generic one.
type Combine<T> = (T, T) => T;
add(a: int, b: int): int { return a + b; }
fold(values: int[], combine: Combine<int>, start: int): int {
var total = start;
for (var v in values) {
total = combine(total, v);
}
return total;
}
# A function that returns nothing has the return type void.
const events = async.Atomic<int>(0);
record(n: int) { events.fetch_add(n); }
main(): int {
var t = Transform();
assert(t.run(4) == 16, "the default op squares");
t.op = negate;
assert(t.op(4) == -4, "call through a field");
var pipeline = Array<(int) => int>();
pipeline.append(increment);
pipeline.append(square);
pipeline.append(negate);
var value = 2;
for (var step in pipeline) {
value = step(value);
}
assert(value == -9, "-((2 + 1) * (2 + 1))");
assert(pipeline[1](5) == 25, "call an array element");
assert(pick(true)(5) == 25, "call a returned function directly");
var chosen = pick(false);
assert(chosen(5) == -5, "call a stored returned function");
assert(handler.load()(1) == 2, "global function value");
handler.store(square);
assert(handler.load()(3) == 9, "replaced global");
var numbers = Array<int>();
numbers.append(1);
numbers.append(2);
numbers.append(3);
assert(fold(numbers, add, 10) == 16, "10 + 1 + 2 + 3");
var callback: (int) => void = record;
callback(2);
callback(5);
assert(events.load() == 7, "void function values");
return 0;
}
- Fields. A field of function type is called like a method:
t.op(4). Inside the struct,op(x)andthis.op(x)both call it. - Arrays.
pipeline[1](5)indexes the array and calls the element. - Module-level variables. Every module-level variable is
constand thread-safe (see Module-scope variables), so one whose function changes is anAtomicof asharedfunction type:handler.load()(1)calls the current function andhandler.store(square)replaces it. - Return values.
pick(true)(5)calls the function thatpickreturns. voidresults. A function without a return value has a function type ending in=> void, such as(int) => void.
Closures
A closure literal is a parameter list, =>, and a body. The body is an
expression or a block:
# Closure literals: anonymous functions that capture values.
struct Counter {
int count;
construct(this.count = 0) {}
add(n: int) { count += n; }
}
apply(f: (int) => int, x: int): int {
return f(x);
}
# A closure can outlive the function that made it: it keeps its own copy
# of 'n'.
make_adder(n: int): (int) => int {
return (x) => x + n;
}
struct Scaler {
int factor;
construct(this.factor = 1) {}
# Inside a method, a closure can use 'this' and the struct's members.
scaler(): (int) => int {
return (x) => x * factor;
}
}
main(): int {
# Parameter types written out: the closure's type is (int) => int.
var double = (x: int) => x * 2;
assert(double(21) == 42, "expression body");
# With a function type to fill, the parameter types can be left out.
var add: (int, int) => int = (a, b) => a + b;
assert(add(40, 2) == 42, "types from the variable");
assert(apply((x) => x * x, 5) == 25, "types from the parameter");
# A block body, with the return type written after the parameters.
var clamp = (x: int, low: int, high: int): int => {
if x < low { return low; }
if x > high { return high; }
return x;
};
assert(clamp(7, 0, 5) == 5, "block body");
# Captured values.
var add_five = make_adder(5);
assert(add_five(10) == 15, "captured parameter");
var offset = 100;
var shift = (x: int) => x + offset;
assert(shift(1) == 101, "captured local");
# A captured struct is shared, so the closure can change the object.
var counter = Counter();
var bump = () => counter.add(1);
bump();
bump();
assert(counter.count == 2, "shared struct");
var triple = Scaler(3).scaler();
assert(triple(4) == 12, "captured this");
# A closure can return a closure.
var adder: (int) => (int) => int = (a) => (b) => a + b;
assert(adder(2)(3) == 5, "curried");
return 0;
}
-
Parameter types.
(x: int) => x * 2writes the parameter type. When the closure goes straight into a slot of function type (a typed variable, a parameter, an assignment, areturn, a field), the types come from that slot, so(a, b) => a + bis enough. A parameter of a generic function counts too: passing(x) => x + 1toapply<T>(f: (T) => T, x: T)withxaninttypes the closure’sxasint(see Generics). With no slot to take them from, a parameter without a type is an error:main.pep:2:14: error: cannot infer the type of parameter 'x'; write it: (x: Type) => ... -
Return type. It is inferred from the body, or from the slot. To write it, put it after the parameters:
(x: int): long => x. -
Body. An expression body is the value the closure returns. A block body uses
returnlike a function. A closure whose type returnsvoidcan have an expression body such as() => counter.add(1); the value is discarded. -
Calling. A closure is called like any function value, and can be called where it is written:
((x: int) => x + 1)(2).
Captured values
A closure can use the local variables and parameters around it. It keeps a
copy of each one it uses, made when the closure is created, so it can outlive
the function that created it (make_adder above).
A captured variable must never be assigned, before or after the closure is created, inside the closure or outside it. Then the copy and the variable can never differ:
var count = 0;
var f = () => count;
count = 5; # error
main.pep:4:5: error: cannot assign to 'count' because a closure captures it
main.pep:3:19: note: captured here; a closure keeps a copy, so the variable must not change
The rule applies to ++, -- and compound assignments too. Only the
variable is fixed: a captured struct is a reference to the same object, so the
closure can call its methods and change its fields (counter above). To share
a changing number, capture a struct that holds it, such as a Counter or an
Atomic<int>.
this in methods
A closure written in a method can use this, the struct’s fields and its
methods. It captures this (one reference to the object), never a copy of
the fields, so it sees the fields’ current values. In a trait’s
default method, a closure
can use this and the trait’s methods the same way.
Storing such a closure in a field of the same object would make the object and the closure keep each other alive forever (a reference cycle), so that direct case is an error:
main.pep:4:15: error: storing a closure that captures 'this' in field 'measure' of 'this' creates a reference cycle that is never freed
Capture a weak reference to the object instead.
Type checking
A function value must match the type it flows into exactly: the same parameter
types in the same order and the same return type. The aliases of a primitive
type count as the same type, so (i32) => i32 and (int) => int match.
A mismatch in an initializer, an assignment, an argument or a return is a
compile error, as is calling a function value with the wrong number of
arguments:
square(x: int): int { return x * x; }
main(): int {
var f: (String) => int = square; # error
return f(1, 2); # error
}
main.pep:4:30: error: cannot initialize variable 'f' of type '(String) => int' with a value of type '(int) => int'
4 | var f: (String) => int = square; # error
| ^~~~~~
main.pep:5:12: error: too many arguments to function of type '(String) => int': expected 1, have 2
5 | return f(1, 2); # error
| ^
The compiler reports one error at a time, so the second appears once the first is fixed.
Sharing function values between threads
A plain function type such as () => void can hold any function value,
including a closure that captures a String or an Array. Such values stay
on the thread that made them: their reference counts are plain increments,
like those of a struct that is not [Shared] (see
What threads can share).
A function type written with shared in front, such as shared () => void
or shared (long) => void, is thread-safe. Its reference counts are atomic,
and its values hold only thread-safe state, so any thread may copy, call and
drop them. It goes wherever a thread-safe type is required: the body of a
thread (ThreadBody is shared () => void), a field of a [Shared] struct,
a module-scope variable, or async.Atomic<shared (int) => int>.
# shared function types: function values that threads can share.
import async;
const total = async.Atomic<long>(0);
# A named function holds no state, so it is always thread-safe.
add_one() {
total.fetch_add(1);
}
# A [Shared] struct's fields must be thread-safe: a function field is shared.
[Shared]
struct Task {
const weight: long;
const run: shared (long) => void;
construct(this.weight = 1, this.run = (n: long) => {}) {}
}
# A plain function type takes any function of the same signature.
apply(f: (long) => long, x: long): long {
return f(x);
}
main(): int {
# A closure converts to a shared type when everything it captures is
# thread-safe: here nothing but the module-scope Atomic.
var add: shared (long) => void = (n: long) => { total.fetch_add(n); };
var task = Task(10, add);
# A thread body is a shared () => void. This closure captures a
# [Shared] struct.
var worker = async.Thread.spawn(() => { task.run(task.weight); });
var other = async.Thread.spawn(add_one);
worker.join();
other.join();
assert(total.load() == 11, "both threads ran");
# A shared value can be used where a plain one is expected.
var step: long = 5;
var add_step: shared (long) => long = (x: long) => x + step;
assert(apply(add_step, 1) == 6, "shared value passed as plain");
# A plain closure may capture anything, but stays on this thread.
var name = "worker";
var greet = () => name + " done";
assert(greet() == "worker done", "plain closure");
return 0;
}
-
What converts to
shared. A named function, which holds no state; a method, constructor or generic function value with nothing captured, such asPoint.normorBox<int>; and a closure literal (or a bound method such asp.norm) whose captured values are all thread-safe: numbers,Atomic<T>and other[Shared]structs,sharedfunction values, and the rest of that list. The value must be written where thesharedtype is expected, so that it is made thread-safe from the start. -
What does not. A closure that captures a value that is not thread-safe is reported at the capture, with a note at the captured variable:
main.pep:5:46: error: closure passed as 'shared () => void' captures 'name' of type 'String', which is not thread-safe 5 | var t = async.Thread.spawn(() => { print(name); }); | ^~~~ main.pep:4:9: note: capture a thread-safe value instead, such as an 'async.Atomic<T>' or a '[Shared]' struct 4 | var name = "worker"; | ^~~~A closure in a method that uses
thisor a member capturesthis, so it is thread-safe only in a[Shared]struct. A value that already has a plain type (a variable, a parameter, a call result) never becomesshared, even when it holds nothing:main.pep:3:32: error: cannot initialize variable 'f' of type 'shared () => void' with a value of type '() => void' 3 | var f: shared () => void = g; | ^ -
From
sharedto plain. Asharedvalue converts to the plain type of the same signature anywhere:shared (long) => longcan be passed to a parameter of type(long) => long. The conversion allocates a small plain function value that holds the shared one and calls it, so the plain copy counts with plain increments while the shared value keeps its atomic ones. -
Inference. A variable initialized with a closure, such as
var f = () => 1;, has a plain type. Write the type to make itshared:var f: shared () => int = () => 1;. -
sharedis special only directly before a function type; elsewhere it is an ordinary name.
Null function values
A function-typed variable or field that has never been assigned is null.
Calling it stops the program with the message called a null function value:
var handler: (int) => int;
handler(1); # panics: called a null function value
Names and shadowing
A local variable, parameter or field with the same name as a free function
hides the function inside its scope. Inside a function with a parameter named
square, square means the parameter, not the function.
Memory
A function value is a reference, like a struct value, and follows the same
reference-counting rules (see Memory and reference counting).
Referring to a named function allocates nothing: every reference to square
shares one static function object, and so does a generic function named
without its type arguments (larger as an (int, int) => int). A method, constructor or generic function
value allocates one small object, like a closure; a bound method’s object
holds a reference to its receiver.
Creating a closure allocates one object holding the captured values, like constructing a struct. Copying the closure adds a reference to that object, and when the last reference goes, the object releases what it captured.
A plain function value’s counts are plain increments; a shared one’s are
atomic, like a [Shared] struct’s (see
Sharing function values between threads).
C function pointers
A C function pointer type, extern (P...) => R, holds a bare address that C
can call, not a function value: it takes a named function or a closure that
captures nothing, and never a closure that captures values. That is by
design; see Passing a Pepper function to C
for how a callback gets the state it needs.
Overloading
Several free functions in one file can share a name when their parameter lists differ. Each call picks the one whose parameters fit its arguments best:
# Overloaded free functions: one name, several parameter lists.
import io;
describe(n: long): String { return "a number"; }
describe(x: double): String { return "a fraction"; }
describe(s: String): String { return "some text"; }
describe(s: String, times: int): String { return "text, repeated"; }
# An exact match beats a widening, which beats a conversion; an
# `[Implicit]` constructor comes last.
size(p: io.Path): int { return 1; }
size(n: long): int { return 2; }
main(): int {
var small: int = 7;
assert(describe(small) == "a number", "int widens to long");
assert(describe(2.5) == "a fraction", "exact match");
assert(describe("hi") == "some text", "a literal promotes to String");
assert(describe("hi", 3) == "text, repeated", "picked by argument count");
assert(size(4) == 2, "an integer literal fits a long");
assert(size("notes.txt") == 1, "only io.Path takes a String, by its [Implicit] constructor");
return 0;
}
The functions of one name in one file are an overload set, and each is a member of it; the files of one module may share a set, and so do the sets a file imports, the runtime’s, and its own functions of that name. Methods of one name in a struct are a set too, and so are its static methods, its operators, its constructors, and the methods its extensions add.
Declaring overloads
-
The parameter lists must differ in the number or the types of the parameters. Parameter names and default values do not count (though named arguments can pick between members that differ in types), and neither does the return type:
main.pep:2:1: error: functions that differ only in their return type cannot be overloaded 2 | f(x: int): long { return 2; } | ^ main.pep:1:1: note: previous declaration is here 1 | f(x: int): int { return 1; } | ^Two functions with the same parameter types and the same return type are a
redefinition of 'f'. -
A file’s own functions join imported ones. A file’s functions of a name and the functions its imports bind to that name are one set (see Imported sets merge).
Files of one module share a set
When an import or a re-export takes one name from two files of the same
module, and both are functions declared under that name, it takes their
union: one set of every member. Here shapes/module.pep re-exports area
from two files:
# Each file adds members to `area`; the module exports one set.
export * from "square";
export * from "rect";
export area(side: int): int {
return side * side;
}
export area(width: int, height: int): int {
return width * height;
}
import "shapes";
main(): int {
assert(area(3) == 9, "square.pep's member");
assert(area(2, 5) == 10, "rect.pep's member");
return 0;
}
A file of the module sees the members its own file and the module’s files it
imports declare (see
Visibility).
That holds for unexported functions too: functions of one name that files of
a module declare, exported or not, are one set wherever those files are
imported. Members from different files must still differ in their
parameters (redefinition of 'area'). Names that are not all functions,
such as two structs Point, and a renaming re-export
(export size as area beside an area) are still exported twice.
Imported sets merge
Sets of one name from different modules merge too, as they do in Swift:
where two imports bind describe, and where the file declares describe
itself, an unqualified describe(…) picks among every member. Here two
modules each export one member and the file adds a third:
export describe(n: int): String {
return "a number";
}
export describe(flag: bool): String {
return "a flag";
}
import "numbers";
import "flags";
# The file's own `describe` joins the two modules' sets.
describe(text: String): String {
return "some text";
}
main(): int {
assert(describe(3) == "a number", "numbers' member");
assert(describe(true) == "a flag", "flags' member");
assert(describe("hi") == "some text", "this file's member");
return 0;
}
Unlike files of one module, two modules may each export a member with the same parameters. That is no error until a call cannot tell them apart:
main.pep:3:22: error: call to 'parse' is ambiguous
3 | main(): int { return parse(1); }
| ^~~~~
a.pep:1:8: note: candidate function
1 | export parse(x: int): int { return 1; }
| ^~~~~
b.pep:1:8: note: candidate function
1 | export parse(x: int): int { return 2; }
| ^~~~~
A qualifier (a.parse(1)) picks one module’s set. A file’s own member
hides an imported member with the same parameters, so a file can replace
one member of an imported set. Only functions merge: a struct or variable
of the name hides imported functions of it (or, named in an import list,
is 'parse' is imported but also declared in this file).
The runtime’s sets
The prelude’s functions, such as the runtime’s print and
to_string, are a set too, which a file’s own and imported functions of
the name join. Here point.pep adds a print member and the file adds a
to_string member, and the runtime’s members still take every other
call:
export struct Point {
var x: int;
var y: int;
construct(x: int, y: int) {
this.x = x;
this.y = y;
}
}
# Joins the runtime's `print(String)` wherever it is imported.
export print(p: Point) {
print($"({p.x}, {p.y})");
}
import "point";
# The file's own `to_string` joins the runtime's.
to_string(p: Point): String {
return $"Point({p.x}, {p.y})";
}
main(): int {
var p = Point(1, 2);
print(p); # point's member
print("text"); # the runtime's member
assert(to_string(p) == "Point(1, 2)", "this file's member");
assert(to_string(42) == "42", "the runtime's member");
return 0;
}
A member may not take a runtime member’s parameters, since a call could not tell the two apart. With the same return type (or one left to inference) it is a redefinition:
main.pep:1:1: error: redefinition of 'print'
1 | print(text: String) {}
| ^~~~~
runtime/string.pep:1850:8: note: previous definition is here
1850 | export print(text: String) {
| ^~~~~
and with another return type it is the same error as for two members of one file:
main.pep:1:1: error: functions that differ only in their return type cannot be overloaded
1 | to_string(value: int): int { return value; }
| ^~~~~~~~~
runtime/string.pep:1621:8: note: previous declaration is here
1621 | export to_string(value: int): String {
| ^~~~~~~~~
Give the function another name or other parameters instead. A struct or variable named like a runtime function hides all of its members.
Members may differ in visibility
Exported members are what other files see; a private member is a candidate
only for calls in its own file, or in its module’s files. Here shapes.pep exports area(int) and
keeps the two-argument form as a helper:
# One set: `area(int)` is exported; `area(int, int)` is private to this file.
export area(side: int): int {
return area(side, side);
}
area(width: int, height: int): int {
return width * height;
}
import "shapes";
main(): int {
# Only the exported member is a candidate here: `area(2, 3)` would be
# an error, since `area(int, int)` is private to shapes.pep.
assert(area(3) == 9, "the exported member");
return 0;
}
A call that only the private member fits is an error in another file, and its notes list only the exported members:
main.pep:2:22: error: no matching function for call to 'area'
2 | main(): int { return area(2, 3); }
| ^~~~
shapes.pep:2:8: note: candidate function not viable: requires 1 argument, but 2 were provided
2 | export area(side: int): int {
| ^~~~
Importing the name (import [area] from "shapes", or the whole file)
brings every exported member, and import [area as size] renames the
whole set. A re-export (export area;, export * from "shapes") passes
on the exported members too; a set whose members are all private is
re-exported whole.
extern functions do not overload
An extern function does not overload, by design: an extern is a C
function, which has one symbol, and keeping one extern per name keeps
the binding to C one-to-one. To put a family of C functions such as
abs and llabs behind one name, give each extern a name of its own
([Link(name = ...)] binds it to the C symbol) and overload Pepper
functions that call them. With optimization on (-O2), LLVM inlines
the small wrappers:
# C's `abs` and `llabs` behind one Pepper name. An extern binds one C
# symbol, so each gets its own name, and Pepper functions overload them.
[Link(name = "abs")]
extern int c_abs(int x);
[Link(name = "llabs")]
extern i64 c_llabs(i64 x);
magnitude(x: int): int { return c_abs(x); }
magnitude(x: long): long { return c_llabs(x); }
main(): int {
assert(magnitude(-3) == 3, "int");
var big: long = -5000000000;
assert(magnitude(big) == 5000000000, "long");
return 0;
}
Overloading the extern itself is an error that says so (extern 'abs' cannot be overloaded; rename it and overload Pepper functions named 'abs' that call it).
How a call picks a member
A member is viable when the call has between its required and total number of parameters and each argument converts to its parameter. For each argument, the conversion has a rank, best first:
| Rank | What converts |
|---|---|
| exact | the same type; a string literal to __ptr<char> or StringConstant |
| promotion | an integer widening that loses nothing (int to long, u8 to int; the narrower target ranks better, so a short picks int over long), float to double, an integer literal to any integer type it fits, a struct to a trait it declares, __null to a reference, a string literal to String |
| conversion | any other numeric conversion (bool included), and pointer conversions |
| implicit constructor | a value to a struct whose [Implicit] constructor takes it |
One viable member is better than another when none of its arguments
ranks worse and at least one ranks better, as in C++. Between two members
whose arguments rank the same, a non-generic one is better than a generic
one, and of two generic ones the more specialized is better: the one
whose parameter types the other’s could take but not the reverse. So
describe<A, B>(pair: (A, B)) takes a tuple argument over
describe<T>(value: T), which takes anything else. The call picks the
member that is better than every other viable one. When there is none, the
call is an error that lists the candidates:
main.pep:3:22: error: call to 'f' is ambiguous
3 | main(): int { return f(1); }
| ^
main.pep:1:1: note: candidate function
1 | f(x: long): int { return 1; }
| ^
main.pep:2:1: note: candidate function
2 | f(x: short): int { return 2; }
| ^
An integer literal fits every integer type that holds its value equally
well, so f(1) above is ambiguous; an int member would take it exactly.
Two widenings to types of the same width (a ushort to long and ulong)
are ambiguous too; convert the argument yourself (f(n as long)). When no
member is viable, each note says why:
main.pep:5:12: error: no matching function for call to 'f'
5 | return f(s);
| ^
main.pep:1:1: note: candidate function not viable: no known conversion from 'String' to 'int' for 1st argument
1 | f(x: int): int { return 1; }
| ^
main.pep:2:1: note: candidate function not viable: requires 2 arguments, but 1 was provided
2 | f(x: int, y: int): int { return 2; }
| ^
A call with named arguments considers only
the members that have a parameter of each name it uses and get every
required parameter, and binds the names before ranking the arguments as
above. So two overloads whose later parameters have different names can be
told apart by naming one (pick(1, b = 2) against pick(a: int, b: int = 0)
and pick(a: String, c: int = 0)). The members it considers must put each
named argument at the same position; when they do not, the call is an error:
named arguments bind differently in the overloads of 'f'.
The member picked decides the call’s type, so var x = half(9.0); is a
double when half(x: double): double is picked. In the editor, hovering
the call shows the member it picked and how many others the set has, and
go-to-definition goes to that member.
Generic members
A set can mix generic and non-generic members. A generic member is viable
when its type arguments can be inferred from the call, and ranks as an exact
match for the arguments that inferred them. When a generic and a non-generic
member tie, the non-generic one wins. Writing type arguments
(describe<int>(7)) considers only the generic members:
# Generic and non-generic overloads, and overloads called from generic code.
# A non-generic overload wins a tie with a generic one.
describe<T>(value: T): String { return "something"; }
describe(value: int): String { return "an int"; }
# Generic overloads can differ in their number of parameters.
last<T>(a: T): T { return a; }
last<T>(a: T, b: T): T { return b; }
kind(n: int): String { return "number"; }
kind(s: String): String { return "text"; }
# Each specialization resolves `kind` for its own `T`.
label<T>(value: T): String { return kind(value); }
main(): int {
assert(describe(7) == "an int", "non-generic wins the tie");
assert(describe(1.5) == "something", "only the generic fits");
assert(describe<int>(7) == "something", "type arguments pick generic overloads");
assert(last(1) == 1, "one argument");
assert(last(1, 2) == 2, "two arguments");
assert(label(3) == "number", "label<int> calls kind(int)");
assert(label("x") == "text", "label<String> calls kind(String)");
return 0;
}
A call inside a generic function or a generic struct’s method whose argument
types depend on a type parameter is resolved again for each specialization,
so label<String> above calls kind(s: String).
An override such
as describe<Point>(p: Point) belongs to the generic member with the same
number of type parameters and parameters; it is not a member itself.
Methods and operators
A struct’s methods of one name are an overload set, and a call picks among
them the same way, whether it is written m.add(x), this.add(x), or
add(x) inside a method. Static methods overload among themselves
(Money.of(250)), and operators are methods, so +(Money) and +(int) can
coexist:
# Overloaded methods, static methods and operators.
struct Money {
var cents: long;
construct(long cents = 0) {
this.cents = cents;
}
# Methods of one name in a struct overload like free functions.
add(other: Money): Money { return Money(cents + other.cents); }
add(dollars: int, cents: int): Money { return Money(this.cents + dollars * 100 + cents); }
# Static methods overload among themselves.
static of(cents: long): Money { return Money(cents); }
static of(text: String): Money { return Money(text.size as long); }
# Operators are methods, so they overload by operand type too.
+(other: Money): Money { return add(other); }
+(more: int): Money { return Money(cents + more); }
}
main(): int {
var a = Money.of(250);
var b = a.add(1, 5); # add(int, int)
assert(b.cents == 355, "two ints");
assert(a.add(b).cents == 605, "a Money");
var c = a + b + 10; # +(Money), then +(int)
assert(c.cents == 615, "operators");
assert(Money.of("four").cents == 4, "static of(String)");
# A method used as a value takes the member its type names.
var plus: (Money) => Money = a.add;
assert(plus(a).cents == 500, "method value");
return 0;
}
-
Unary and binary operators of one symbol are one set: a unary
-()and a binary-(other: Money)are told apart by their number of parameters (see Unary operators). -
A static method and an instance method may not share a name, since a bare
of(x)inside a method could mean either:main.pep:3:12: error: static method 'get' cannot share its name with an instance method of 'S' 3 | static get(x: String): int { return 2; } | ^~~ main.pep:2:5: note: previous declaration is here 2 | get(x: int): int { return 1; } | ^~~ -
In a generic struct, each specialization resolves its own calls: in
Holder<int>,has(x: T)takes anint. Two members that become the same after substitution (has(x: T)andhas(x: String)inHolder<String>) are only an error when a call cannot tell them apart. -
A method as a value (
m.add,Money.of) picks its member from the expected function type, like a free function below; without one the error iscannot infer the type of 'f' from overloaded method 'Money.add'.
Extensions and traits
An extension’s methods join the struct’s overload set of their name, and a trait may declare several methods of one name. Each of a trait’s overloads is implemented by the conforming type’s method with the same parameter types, and a call through a value of the trait type picks among the trait’s members the same way:
struct Square {
var side: int = 0;
construct(this.side = 0) {}
area(): int {
return side * side;
}
}
# An extension's methods join the struct's overload set of their name.
extend Square {
area(scale: int): int {
return area() * scale;
}
}
# A trait may declare overloads too; each is its own member.
trait Shape {
area(): int;
area(scale: int): int;
# A default joins the conforming type's set of its name.
describe(): int {
return area() * 100 + area(2);
}
}
# Each trait member is implemented by the method with its parameter types.
extend Square with Shape;
total(shape: Shape): int {
return shape.area() + shape.area(10);
}
main(): int {
var square = Square(3);
assert(square.area(2) == 18, "the extension's overload");
assert(total(square) == 99, "each call through the trait takes its own member");
assert(square.describe() == 918, "the default body calls both");
return 0;
}
- An extension member with a body method’s parameters is
redefinition of 'area', with a note at the method it repeats. - A call sees only the extensions visible in its file (see Visibility): a private extension’s overload is never a candidate in another file, so importing a file can add candidates to a call. If an added candidate makes a call ambiguous, the error’s notes point at each candidate, the extension’s included.
- A trait’s default method is copied into a conforming type that lacks a method with its parameter types, even when the type has other methods of that name; the copy joins their set.
- A missing overload is named with its parameter types:
'Square' does not implement 'area(int)', required by trait 'Shape'.
Constructors
A struct may declare several constructors. A call Span(...) picks among
them exactly as a call picks among functions:
struct Span {
var start: int = 0;
var length: int = 0;
# Constructors overload like functions: each takes its own parameters.
construct(this.length = 0) {}
construct(start: int, end: int) {
this.start = start;
length = end - start;
}
construct(text: String) {
length = text.length as int;
}
}
main(): int {
assert(Span().length == 0, "no arguments");
assert(Span(5).length == 5, "one int");
var middle = Span(2, 7);
assert(middle.start == 2 && middle.length == 5, "two ints");
assert(Span("pepper").length == 6, "a String");
# As a value, the expected function type picks the constructor.
var make: (int, int) => Span = Span;
assert(make(1, 4).length == 3, "the (int, int) constructor");
return 0;
}
- Two constructors with the same parameter types are
constructor of 'Span' cannot be redeclared, with a note at the first. - No constructor fits is
no matching constructor for initialization of 'Span', and a tie iscall to constructor of 'Span' is ambiguous; the notes point at eachcandidate constructor. - A generic struct’s constructors resolve in each specialization:
Pair<int>(1, 2)picks amongPair<int>’s constructors. - At most one constructor may be
[Implicit]: an implicit conversion needs one constructor to call. [Deprecated]on one constructor warns only at the calls that resolve to it.- A constructor as a value (
var make: (int, int) => Span = Span;) picks its member from the expected function type, like a function below.
Overloaded functions as values
The name of an overload set, written without a call, is a value only where
the expected function type picks one member exactly: a typed variable, an
argument, a return from a function with a declared return type, or an
assignment. A closure whose parameters have no types, passed to an
overloaded function, picks the member whose function parameter has its
number of parameters:
# An overloaded function used as a value, and closures passed to overloads.
twice(n: int): int { return n * 2; }
twice(s: String): String { return s + s; }
apply(f: (int) => int, n: int): int { return f(n); }
# Overloads that differ in the type of a function parameter.
run(f: (int) => int): int { return f(10); }
run(f: (int, int) => int): int { return f(10, 20); }
main(): int {
# The declared type picks the overload.
var doubler: (int) => int = twice;
assert(doubler(4) == 8, "twice(int)");
# So does the parameter it is passed to.
assert(apply(twice, 5) == 10, "twice(int) again");
# A closure without parameter types picks the overload of its arity.
assert(run((a) => a + 1) == 11, "one parameter");
assert(run((a, b) => a + b) == 30, "two parameters");
return 0;
}
Without an expected type, the compiler asks for one:
main.pep:4:13: error: cannot infer the type of 'g' from overloaded function 'f'
4 | var g = f;
| ^
main.pep:1:1: note: candidate function
1 | f(x: int): int { return 1; }
| ^
main.pep:2:1: note: candidate function
2 | f(x: long): int { return 2; }
| ^
main.pep:4:13: note: declare the type, for example 'var g: (int) => int = f;'
4 | var g = f;
| ^
An expected type that matches no member is address of overloaded function 'f' does not match required type '(bool) => int'.
Structs
A struct groups related values into one named type, and gives that type behavior through methods. Pepper has no classes and no inheritance: a struct holds data, methods and extensions add behavior, and traits describe what a type can do.
Every struct value is a reference to an object, so assigning a struct or passing it to a function shares the object rather than copying it. The memory chapter explains exactly when objects are created and destroyed.
Declaring a struct
Declare a struct with struct, a name, and its fields in braces. Struct
names are PascalCase by convention.
# A struct groups related values into one type.
struct Point {
var x: int = 0;
var y: int = 0;
}
# Fields can also be declared C-style, with the type first.
struct Counter {
int count; # no initializer: starts at zero
String label = "clicks";
}
main(): int {
# Without a constructor, `Point()` makes a Point with every field
# at its initial value.
var p = Point();
assert(p.x == 0 && p.y == 0, "fields start at their initializers");
p.x = 3;
p.y = 4;
assert(p.x * p.x + p.y * p.y == 25, "fields can be read and written");
var c = Counter();
assert(c.count == 0, "a numeric field without an initializer is zero");
assert(c.label == "clicks", "a String field can have an initializer");
# A struct value is a reference: `q` names the same Point as `p`.
var q = p;
q.x = 10;
assert(p.x == 10, "p and q are the same object");
assert(p === q, "=== tests whether two references name the same object");
assert(!(p === Point()), "a new Point is a different object");
return 0;
}
Things to notice:
- A field is declared like a variable:
var x: int = 0;or, C-style,int x = 0;. The initializer is optional. - A field without an initializer starts at its type’s default: zero for
numbers,
falseforbool, and null for a struct type such asStringor another struct. Reading a field through a null reference crashes the program, so give struct-typed fields a value before you use them. - A struct with no constructor can be created with its name and empty
parentheses,
Point(). There is nonewkeyword at the call site. p.xreads a field andp.x = 3writes it.a === bis true whenaandbrefer to the same object, anda !== bwhen they do not. See Comparing structs for==.
A struct can have a field whose type is a struct declared later in the file,
a field of its own type, or fields that lead back to it through other
structs (a Team holding a Player that holds the Team). A struct-typed
field holds a reference, so a linked list or a tree is just a struct that
names itself:
# A field can hold the struct's own type: each Link points at the next one.
struct Link {
var value: int;
var next: Link;
construct(this.value = 0) {}
}
# Two structs can refer to each other, in either declaration order.
struct Team {
var name: String;
var lead: Player;
construct(this.name = "") {}
}
struct Player {
var name: String;
# A weak back pointer: the team owns its lead, so the player's link
# back to the team must not keep the team alive (see the memory chapter).
var team: Weak<Team>;
construct(this.name = "") {}
}
sum(first: Link): int {
var total = 0;
var link: Link = first;
while (link !== __null) {
total = total + link.value;
link = link.next;
}
return total;
}
main(): int {
var list = Link(1);
list.next = Link(2);
list.next.next = Link(3);
assert(sum(list) == 6, "the list holds 1, 2 and 3");
assert(list.next.next.next === __null, "the last link has no next");
var team = Team("Owls");
team.lead = Player("Ada");
team.lead.team = Weak<Team>(team);
with (var t = team.lead.team) {
assert(t.name == "Owls", "the player reaches its team");
}
return 0;
}
Objects that refer to each other in a loop keep each other alive, so make
back pointers such as a child’s parent a Weak<T> (see
Cycles leak).
Constructors
A constructor sets up a new object. Declare it with construct, followed by
its parameters and a body. Callers create the object by calling the struct’s
name like a function.
struct Rect {
var width: int;
var height: int;
var area: int;
# `this.width = 1` declares a parameter that is stored straight into the
# field `width`; `1` is its default when the caller leaves it out.
construct(this.width = 1, this.height = 1) {
area = width * height;
}
}
struct Temperature {
var celsius: float;
# Ordinary parameters work too, written like a function's: `name: Type`.
construct(fahrenheit: float = 32.0) {
celsius = (fahrenheit - 32.0) * 5.0 / 9.0;
}
}
main(): int {
var r = Rect(3, 4);
assert(r.width == 3 && r.height == 4, "arguments fill the fields");
assert(r.area == 12, "the constructor body runs after the fields are set");
var tall = Rect(2);
assert(tall.width == 2 && tall.height == 1, "omitted arguments use their defaults");
var unit = Rect();
assert(unit.area == 1, "every parameter has a default, so Rect() works");
var boiling = Temperature(212.0);
assert(boiling.celsius == 100.0, "212 F is 100 C");
assert(Temperature().celsius == 0.0, "the default is 32 F");
return 0;
}
- A parameter written
this.width = 1stores its argument directly into the fieldwidth, before the body runs. The= 1is required: it is the value used when the caller omits that argument. - Ordinary parameters are written as in a function,
name: Type, with an optional default:fahrenheit: float = 32.0. The C-style form,float fahrenheit = 32.0, is accepted too. - Inside the body, fields are in scope by name. Use
this.namewhen a parameter has the same name as a field. - A struct may declare several constructors with different parameter
lists; a call picks one by its arguments, as for any
overloaded function. At most one of them
may be
[Implicit]. A struct has at most one destructor.
A this.field parameter must have a default. Leaving it out is an error:
struct Point {
var x: int;
construct(this.x) {}
}
main.pep:3:21: error: expected '='
3 | construct(this.x) {}
| ^
A struct without a constructor accepts no arguments:
main.pep:7:13: error: too many arguments to 'Point': expected 0, have 2
7 | var p = Point(1, 2);
| ^~~~~
Note:
newis the older, deprecated spelling ofconstruct. It still compiles, with a warning that offers the fix (see Deprecations).
Implicit conversions
A constructor marked [Implicit] also converts. Wherever a value flows into a
place whose type is the struct, and the value has the constructor’s parameter
type, the compiler calls the constructor for you:
struct Celsius {
var degrees: double;
# `[Implicit]` lets a `double` become a `Celsius` wherever one is expected.
[Implicit]
construct(double degrees) {
this.degrees = degrees;
}
}
struct Label {
var text: String;
# Without `[Implicit]`, callers write `Label("...")` themselves.
construct(String text) {
this.text = text;
}
}
describe(t: Celsius): String {
return (t.degrees > 30.0) ? "hot" : "mild";
}
main(): int {
assert(describe(35.0) == "hot", "an argument converts");
var today: Celsius = 21.5; # a typed variable converts
assert(describe(today) == "mild", "already a Celsius");
var label = Label("kitchen"); # no conversion: call the constructor
assert(label.text == "kitchen", "explicit constructor call");
var n = 12.0; # no slot type: stays a double
assert(n == 12.0, "untyped local");
return 0;
}
The standard library uses this for io.Path,
which converts from a String, so dir.join("logs") and
io.File.open("notes.txt") work without writing io.Path(...), and for
Error, so Result<int>.err("bad input") works.
- Where it applies: a variable’s initializer when the variable’s type is
written, an assignment, a function, method or constructor argument
(operators and
a[i] = valueincluded), a call through a function value, and areturnfrom a function with a declared return type. - Where it does not: the receiver of a method call (
"docs".join("x")does not turn"docs"into aPath), avarwhose type is inferred (var n = 12.0;stays adouble), and__null, which stays a null reference. - One conversion per value. The value must already have the
constructor’s parameter type, or reach it by a widening that loses nothing:
a smaller integer to a larger one, an integer literal that fits,
floattodouble, or a struct to a trait it declares. Conversions never chain: ifTagconverts fromNameandNamefromString, aStringdoes not become aTag. - Overloads prefer anything else. Among overloads,
a member that takes the value as it is, or by any other conversion, beats
one that needs an
[Implicit]constructor. - Each conversion is a constructor call, so it allocates a new object. The VS Code extension shows the call as an inlay hint around the converted value, and hovering the value names the constructor. The converted value is a temporary like any constructor call’s result: it is released after the call it was passed to (see Memory and reference counting).
- Only a constructor that can be called with one argument may be
[Implicit]: one parameter, or a first parameter followed by parameters that all have defaults. It may not convert from the struct itself or, in a generic struct, from a bare type parameter (construct(T value)):
main.pep:2:5: error: 'Implicit' constructor of 'Span' must be callable with one argument
2 | [Implicit]
| ^~~~~~~~~~
Conversions are opt-in: a constructor without [Implicit] never runs on its
own. When a value would fit such a constructor, the type error says so:
main.pep:6:27: error: cannot pass a value of type 'String' as argument 1 of 'show', which has type 'Label'
6 | main(): int { return show("x"); }
| ^~~
main.pep:3:5: note: 'Label' has a constructor from 'String': call 'Label(...)', or mark the constructor [Implicit]
3 | construct(String text) { this.text = text; }
| ^~~~~~~~~
Destructors
A destructor runs when an object is destroyed, which happens when the last
reference to it goes away. Declare it with destruct and a body, with no
parentheses. Use it to release something the object holds that Pepper does not
manage for you, such as a file handle from C.
import async;
# A module-scope counter: const, and changed through an Atomic.
const closed = async.Atomic<int>(0);
struct Connection {
var name: String;
construct(this.name = "") {}
# The destructor runs once, when the last reference to the object goes
# away. Use it to release whatever the object holds.
destruct {
closed.fetch_add(1);
}
}
main(): int {
{
var c = Connection("db");
assert(closed.load() == 0, "still in use");
}
# `c` went out of scope at the closing brace.
assert(closed.load() == 1, "the destructor ran at the end of the block");
return 0;
}
You never call a destructor yourself. After it runs, Pepper releases the object’s fields, so a destructor can still read them. The memory chapter lists every point where an object can be destroyed.
Methods
A method is a function declared inside a struct. You call it on an object with
a dot, v.length_squared(), and inside it this refers to that object.
struct Vector {
var x: int = 0;
var y: int = 0;
construct(this.x = 0, this.y = 0) {}
# A method: name, parameters, then the return type after `:`.
length_squared(): int {
# Fields can be named directly inside a method...
return x * x + y * y;
}
# ...or through `this`, the object the method was called on.
dot(other: Vector): int {
return this.x * other.x + this.y * other.y;
}
# Methods can change fields, and can call other methods.
scale(factor: int) {
x = x * factor;
y = y * factor;
}
# The return type can also come first, C-style.
bool is_zero() {
return length_squared() == 0;
}
# A static method belongs to the type, not to one object: there is
# no `this`. Call it on the type name.
static unit_x(): Vector {
return Vector(1, 0);
}
}
main(): int {
var v = Vector(3, 4);
assert(v.length_squared() == 25, "method call");
assert(v.dot(Vector(1, 1)) == 7, "method with an argument");
v.scale(2);
assert(v.x == 6 && v.y == 8, "a method can modify its object");
assert(Vector().is_zero(), "methods can be called on a temporary");
assert(Vector.unit_x().x == 1, "static method called on the type");
return 0;
}
- Methods are written like functions:
name(parameters): ReturnType { ... }. Leave out the return type for a method that returns nothing. The C-style form,bool is_zero() { ... }, works too. - Inside a method, the object’s fields and other methods can be named
directly (
x,length_squared()), or throughthis(this.x). A parameter or local with the same name as a field hides the field; usethis.to reach the field. - A bare call names the struct’s own method before a top-level function of
the same name, unless no method by that name takes those arguments: then
it calls the function. So a
copy(): Buffermethod can call the free functioncopy(label). - A method can change the fields of its object. Because struct values are references, the caller sees the change.
- Calling a method does not copy the object or add a reference to it. The result of a method call is a new reference, like any function result.
- Several methods can share a name when their parameters differ; each call picks the best fit (see Overloading).
Static methods
A method marked static belongs to the type rather than to one object. It has
no this, and you call it on the type name: Vector.unit_x(). Static methods
are the usual way to offer named constructors, such as the standard library’s
Result<int>.ok(42).
Inside the struct, any method, static or not, can call a static method by
its bare name: unit_x() in a method of Vector means Vector.unit_x().
# Static methods belong to the type; inside it, they are called by name.
struct Vector {
var x: double = 0.0;
var y: double = 0.0;
construct(this.x = 0.0, this.y = 0.0) {}
static unit_x(): Vector {
return Vector(1.0, 0.0);
}
# A static method calls another by its bare name.
static scaled_unit_x(factor: double): Vector {
var unit = unit_x();
return Vector(unit.x * factor, unit.y * factor);
}
}
main(): int {
var v = Vector.scaled_unit_x(3.0);
assert(v.x == 3.0 && v.y == 0.0, "called on the type name outside it");
return 0;
}
Operators
A struct can define what the operators do for it. Declare a method whose name
is the operator, written like any other method: ==(other: Money): bool.
The C-style form with the return type first, bool ==(Money other), is
accepted too.
struct Money {
var cents: int = 0;
construct(this.cents = 0) {}
# `a == b` calls `a.==(b)`. Without this method, `==` on two structs
# compares identity, like `===`.
==(other: Money): bool {
return cents == other.cents;
}
# `!=` is derived from `==` when the struct does not define it.
# `a < b` calls `a.<(b)`.
<(other: Money): bool {
return cents < other.cents;
}
# `a + b` calls `a.+(b)` and returns a new Money.
+(other: Money): Money {
return Money(cents + other.cents);
}
# `-a` calls `a.-()`: a unary operator takes no parameters.
-(): Money {
return Money(0 - cents);
}
# `a - b` calls `a.-(b)`. The unary and binary `-` overload each other,
# told apart by their number of parameters.
-(other: Money): Money {
return Money(cents - other.cents);
}
# `a += b` needs its own method; it is not rewritten to `a = a + b`.
+=(other: Money): Money {
cents += other.cents;
return this;
}
}
struct Shelf {
var slots: Array<String>;
construct() {
slots = Array<String>();
slots.append("empty");
slots.append("empty");
}
# An indexer with one parameter handles reads: `shelf[i]`.
[](index: int): String {
return slots[index];
}
# An indexer with two parameters handles writes: `shelf[i] = value`.
[](index: int, value: String): String {
slots[index] = value;
return value;
}
}
main(): int {
var price = Money(250);
assert(price == Money(250), "== compares cents");
assert(price != Money(100), "!= compares cents");
# `===` and `!==` always compare identity: equal cents, different objects.
assert(price !== Money(250), "!== compares identity");
var same = price;
assert(same === price, "=== is true for the same object");
var total = price + Money(50);
assert(total.cents == 300, "+ makes a new value");
assert(price.cents == 250, "+ left its operands alone");
assert(price < total, "< compares cents");
var refund = -price;
assert(refund.cents == -250, "unary - calls -()");
assert((total - price).cents == 50, "binary - calls -(other)");
total += Money(25);
assert(total.cents == 325, "+= updated total in place");
var shelf = Shelf();
shelf[1] = "books";
assert(shelf[1] == "books", "indexer write, then read");
assert(shelf[0] == "empty", "the other slot is unchanged");
return 0;
}
When the left operand is a struct that declares the operator, a + b becomes
the call a.+(b). The right operand is the method’s single parameter, and it
can be any type. These binary operators can be defined:
| Kind | Operators |
|---|---|
| Arithmetic | + - * / % ** |
| Bitwise | & | ^ |
| Bitwise shifts | << >> |
| Comparison and logical | == != < <= > >= && || |
| Compound assignment | += -= *= /= %= <<= >>= &= |= ^= |
Keep these rules in mind:
- An operator can be declared once per right-operand type (
+(Money)and+(int)), anda + bpicks byb’s type, as for any overloaded method. - Each operator is its own method, with one exception: when a struct defines
==but not!=,a != bmeans!(a == b). A struct that defines==and is used as aHashMaporHashSetkey also needs a matchinghash<T>(see Hashing and equality).+=is not rewritten toa = a + b; define each one you want to use. - Comparison operators must return
bool; any other return type is an error (comparison operator '==' must return 'bool', not 'String'). Logical operators should returnbooltoo. Arithmetic, bitwise and compound assignment operators usually return the struct’s own type; a compound assignment usually changesthisand returns it. - The left operand decides:
a + buses the method ona’s type. For a number on the left, such as2 * v, see A number on the left. - Operators that start with
<(<,<=,<<,<<=) can only be declared in thename: Typeform,<(other: V): bool: in the C-style form the parser readsbool <as the start of generic arguments.
The result of an operator has the type its method returns, so with
-(other: V): int, var r = a - b; is an int.
A number on the left
2.0 * v uses the operator of the left operand’s type, double, which a
struct cannot change. Declare it in an
extension of the
number type instead, where this is the number:
# A number on the left of an operator: `2 * v` with a struct `v`.
struct Vector {
var x: double;
var y: double;
construct(this.x = 0.0, this.y = 0.0) {}
# `v * 2.0` calls `v.*(2.0)`.
*(scale: double): Vector {
return Vector(x * scale, y * scale);
}
}
# `2.0 * v` uses the operator of the left operand's type, so `double`
# declares it in an extension; `this` is the number.
extend double {
*(v: Vector): Vector {
return v * this;
}
}
# `2` is an `int`, which needs its own.
extend int {
*(v: Vector): Vector {
return v * (this as double);
}
}
main(): int {
var v = Vector(1.0, 2.0);
var a = v * 2.0;
var b = 2.0 * v;
var c = 2 * v;
assert(a.x == 2.0 && b.x == 2.0 && c.y == 4.0, "both orders");
# Numbers on both sides still use the built-in operator.
assert(2 * 3 == 6, "built-in");
return 0;
}
-
The number type is the left operand’s own:
2is anintand2.0adouble, so each needs its own extension (extend int,extend double). Without one,2 * visinvalid operands to binary expression ('int' and 'Vector'). -
The operator takes one operand, which may be any type except a number,
bool, enum or pointer: numbers on both sides always use the built-in operator.main.pep:2:14: error: operator '+' of 'int' cannot take an operand of type 'int': the built-in operator applies 2 | +(other: int): int { return 0; } | ^~~ -
Comparisons work the same way, and
2.0 != vmeans!(2.0 == v)when the extension declares only==. Compound assignments (n *= v) do not apply, since the number cannot hold the result. -
The extension is visible like any other: where its file is imported, if it is exported.
Unary operators
A unary operator is a method with no parameters; its operand is this.
| Operator | Declaration | op x calls |
|---|---|---|
| Negation | -(): V | x.-() |
| Bitwise complement | ~(): V | x.~() |
| Logical not | !(): bool | x.!() |
| Truth test | !!(): bool | x.!!() (see below) |
The C-style form works here too: V -(), bool !!(). ! and !! must
return bool (truth-test operator '!!' must return 'bool', not 'int'), and
!, ~ and !! take no parameters. -a on a number is unchanged. A struct
may declare both a unary -() and a binary -(other: V): they
overload each other, told apart by their number of
parameters.
Truth tests and conditions
A condition asks a value “true or false?”. For bool the answer is the value,
for a number it is “not zero”, and for a raw pointer (__ptr<T>) or a function
value it is “not null”. A struct answers through its truth-test operator,
!!(): bool, like C++’s operator bool:
# A struct that declares `!!(): bool` can be used as a condition.
struct Budget {
var cents: int = 0;
construct(this.cents = 0) {}
# The truth test: `if (budget)` calls `budget.!!()`.
!!(): bool {
return cents > 0;
}
}
struct Label {
var text: String;
construct(this.text = "") {}
}
spend(budget: Budget, amount: int): int {
var spent = 0;
# A condition, an operand of `&&`, and the operand of `!` all call `!!`.
while (budget && amount > 0) {
budget.cents -= 1;
amount -= 1;
spent += 1;
}
if (!budget) {
print("budget used up");
}
return spent;
}
main(): int {
var budget = Budget(3);
assert(spend(budget, 2) == 2, "spent 2 of 3");
assert(budget ? true : false, "a ternary condition calls !! too");
assert(spend(budget, 5) == 1, "only 1 left");
assert(!!budget == false, "!!x calls the truth test");
# A struct without `!!` is not a condition: `if (label)` is an error.
# Compare with `__null` to ask whether a reference is set.
var label: Label;
assert(label === __null, "no Label yet");
label = Label("sale");
assert(label !== __null, "now there is one");
return 0;
}
These are the boolean contexts. In each, a struct value x becomes x.!!():
- the condition of
if,else ifandwhile; - the condition of
condition ? a : b; - each operand of
&&,||,andandor(unless the struct declares&&or||itself, which then takes the whole operation); - the operand of
!: without its own!method,!xis!(x.!!()).
!!x is the truth test itself: on a struct it calls x.!!(); on a bool or
a number it is still two negations. Optional declares !!,
so if (found) asks whether an optional holds a value.
A struct that does not declare !! cannot be used as a condition at all:
main.pep:6:9: error: 'Node' cannot be used as a condition: declare '!!(): bool' on it, or compare with '__null'
6 | if (node) {
| ^~~~
A struct value is a reference, but a condition never tests it for null. To ask
whether a reference is set, compare it with __null (node !== __null), or
better, use an Optional for a value that may be missing. A trait value is
treated the same way: it is a condition only when its trait declares !!.
Comparing structs
Without an == method, a == b on two structs compares identity: it is true
only when both refer to the same object, exactly like a === b. The same goes
for !=. Define == when two distinct objects with the same contents should
count as equal, as String and Array do; != then follows from it. To
compare field by field, mark the struct
[Equatable], which derives ==
and hash together. === and
!== always compare identity, whatever methods the struct declares, so
"a" + "b" == "ab" is true while "a" + "b" === "ab" is false.
A struct never equals a number: unless the struct declares an == (or !=)
taking one, comparing a struct value such as an Optional with an integer is
an error (cannot compare 'Optional<ulong>' with an integer), since it could
only ever compare the object itself. Test an optional with is_empty() (or
if (found)) and read its value with value().
Indexers
An indexer lets you write object[index]. Declare it as a method named []:
- With one parameter,
[](index: int): String, it handles reads:shelf[i]. - With two parameters,
[](index: int, value: String): String, it handles writes:shelf[i] = value. The second parameter is the value being stored.
The C-style form, String [](int index), is accepted too. Only parameters
without a default count, so either kind can end with a defaulted parameter,
such as the location: __ptr<char> = __caller_location() that Array uses to
report an out-of-range index at the caller’s line (see
Assertions and panics).
A struct can have both. With both, compound assignment through an index
(counts[i] += 1) reads with the first and writes with the second.
Array and String are
indexed this way.
Assignment through a chain of indexes, library["fiction"][0] = "Dune", reads
with every index but the last and writes with the last, as if the value read
were held in a variable first (var fiction = library["fiction"]; fiction[0] = "Dune";). Since a struct is held by reference, the write changes
the object the outer struct holds. Assigning through an index whose type has
only the reading indexer is an error (cannot assign through '[]': 'Shelf' declares a '[]' getter but no setter).
# A struct declaring `[]` can be indexed: one parameter reads, two write.
struct Shelf {
var titles: Map<String, String>;
var sections: Map<String, Shelf>;
construct() {
titles = Map<String, String>();
sections = Map<String, Shelf>();
}
# `shelf["name"]` reads a section.
[](name: String): Shelf {
return sections[name];
}
# `shelf["name"] = section` writes one.
[](name: String, section: Shelf): Shelf {
sections[name] = section;
return section;
}
# `shelf[slot]` and `shelf[slot] = title` overload `[]` by type.
[](slot: ulong): String {
return titles[$"{slot}"];
}
[](slot: ulong, title: String): String {
titles[$"{slot}"] = title;
return title;
}
}
main(): int {
var library = Shelf();
library["fiction"] = Shelf();
# A chained assignment writes into the shelf the first index reads,
# like `var fiction = library["fiction"]; fiction[0] = ...;`.
library["fiction"][0] = "Dune";
library["fiction"]["classics"] = Shelf();
library["fiction"]["classics"][0] = "Emma";
assert(library["fiction"][0] == "Dune", "set through two indexers");
assert(library["fiction"]["classics"][0] == "Emma", "and through three");
return 0;
}
Properties
A property is a struct member that is read and written like a field
(account.name, account.name = "Savings") but whose get and set
accessors can run code. A property that needs somewhere to keep its value
gets it from the compiler: its implied storage, reachable as field
inside its accessors. You declare a separate private field only when you
want one.
Declaring properties
A property is a field declaration followed by an accessor block instead of
;:
# A property is read and written like a field, but its `get` and `set`
# accessors can run code.
struct Account {
# An auto property: implied storage, read and written by anyone.
var owner: String { get; set; }
# Read-only outside the struct: only Account's own code assigns it.
var balance: long { get; }
# A custom `set` over implied storage: `field` is the storage and
# `value` is the new value.
var name: String {
get;
set {
assert(!value.is_empty, "an account name cannot be empty");
field = value;
}
} = "unnamed";
# Computed: no accessor is omitted and none uses `field`, so it takes
# no space.
var is_empty: bool {
get {
return balance == 0;
}
}
# Explicit storage, when you want it.
var _deposits: ulong;
var deposits: ulong {
get {
return _deposits;
}
}
# An initializer initializes the implied storage.
var limit: long { get; set; } = 100;
construct(this.owner = "") {}
deposit(amount: long) {
balance += amount; # Account's own code may set a get-only property
_deposits += 1;
}
}
main(): int {
var account = Account("Ann");
assert(account.owner == "Ann", "a constructor parameter can set a property");
assert(account.is_empty, "a computed property runs its get");
account.owner = "Bob";
account.name = "Savings";
assert(account.name == "Savings", "the set stored the name");
account.deposit(30);
account.deposit(12);
assert(account.balance == 42 && account.deposits == 2, "methods update the storage");
assert(!account.is_empty, "the computed property follows the balance");
# `account.balance = 0;` would not compile: 'balance' has no 'set'.
account.limit += 50; # a get, then a set
assert(account.limit == 150, "compound assignment reads and writes");
return 0;
}
get;andset;with no body are the defaults:getreturns the storage, andsetstores the new value in it. Either may have a body instead.- A property has implied storage when an accessor is omitted (
get;orset;) or an accessor body usesfield. Otherwise it is computed: it takes no space, and itsgetruns every time it is read. - An initializer after the accessors (
var limit: long { get; set; } = 100;) initializes the storage, so it needs a property that has storage. It does not runset. - Inside an accessor,
fieldis the storage, and insideset,valueis the new value (a by-value parameter). Both are contextual: outside accessors they are ordinary names, so a local or a member calledfieldorvalueelsewhere keeps working. Inside an accessor, a barefieldalways means the storage; reach a member of that name asthis.field. - A property must have a
get:{ set; }alone is an error. - A property is declared with
varand an explicit type; it cannot beconst. There are nostaticproperties.
Reading and writing
p.countcallsget, andp.count = xcallsset. A default accessor is not a call at all: an auto property ({ get; set; }) compiles to the same loads and stores as a field.p.count += 1,p.count++and the other compound assignments are agetand then aset.pis evaluated once.- A constructor parameter
this.nameassigns the property, through itssetwhen it has one. A field initializer and the struct’s own field initialization do not. - Inside the struct’s methods, a property is reached by its bare name, like a
field:
balance += amount;isthis.balance += amount;. - A property’s accessors are ordinary method calls for
reference counting:
getreturns a temporary that owns one reference, andvalueis a by-value parameter, acquired on entry and released on exit.
Read-only properties
{ get; } alone makes a property read-only outside the struct: assigning
it from other code is an error at the property’s name.
main.pep:6:7: error: cannot assign to 'balance': it has no 'set' accessor
6 | a.balance = 3;
| ^~~~~~~
main.pep:2:9: note: 'balance' declared here
2 | var balance: long { get; }
| ^~~~~~~
The struct’s own constructors and methods may assign a get-only property with implied storage; that writes the storage directly. An extension’s methods are not the struct’s own code: they cannot assign it either.
Properties in traits and extensions
A trait can require a property, var area: double { get; } or { get; set; }, with no accessor bodies. A struct meets
the requirement with a property of the same name and type (auto or
computed), or with a plain field. Reading or writing the property through a
trait value calls the struct’s accessors.
An extension can add computed properties only, since it cannot add storage to the struct:
# A trait can require a property; a struct meets it with a property of the
# same name and type.
trait Shape {
var area: double { get; }
var label: String { get; set; }
}
struct Square {
var side: double { get; set; } = 1.0;
var area: double {
get {
return side * side;
}
}
var label: String { get; set; } = "square";
}
extend Square with Shape;
# An extension can add computed properties, which need no storage.
extend Square {
var perimeter: double {
get {
return side * 4.0;
}
}
}
describe(shape: Shape): String {
shape.label = "seen";
return $"{shape.label}: {shape.area}";
}
main(): int {
var square = Square();
square.side = 3.0;
assert(square.area == 9.0, "the computed area follows the side");
assert(square.perimeter == 12.0, "an extension's property reads like any other");
assert(describe(square) == "seen: 9.0", "a trait value reaches the struct's accessors");
assert(square.label == "seen", "the set through the trait reached the struct");
return 0;
}
An extension property that would need storage is an error:
main.pep:5:9: error: an extension cannot add storage: property 'y' must be computed, with accessor bodies that do not use 'field'
An enum can declare computed properties too, under
the same rule, and no set, since its values never change.
Properties of [Shared] structs
The fields of a [Shared] struct are const, so the storage
of its properties is too:
- A property with implied storage must be
{ get; }, and only the struct’s constructors assign it. Aset, with or without a body, is an error atset, with a note at[Shared]. - A computed
getover the struct’sconstfields is allowed, as any method is.
A [Shared] struct’s properties are get only, by design: an assignment
x.p = v would read as a write to a struct whose fields cannot change. To
change what a thread-safe member holds (an Atomic<int>, or a value behind a
Mutex), declare a method that says what it does, such as
store_hits(value) or add_name(name).
How properties compile
The compiler lowers a property to plain members: a field for its storage
(named after the property when its get is the default, __field_name
otherwise), a getter method __get_name() and, with a set, a setter
__set_name(value). Every read and write of the property becomes a storage
access or an accessor call once the receiver’s type is known. The accessor
methods appear in debuggers and in profiles under those names.
Value structs
A struct is normally a reference to an
object on the heap. A struct marked [Value] is a value instead, like a
number or a tuple: it is stored inline in whatever holds it (a
local, a field, an array element), and assigning it, passing it, or
returning it copies it. Creating one allocates nothing.
# A value struct is stored inline and copied like a number.
[Value]
struct Span {
var start: int;
var end: int;
construct(this.start = 0, this.end = 0) {}
length(): int {
return end - start;
}
# Assigns a field of `this`, so it changes the value it is called on.
grow(by: int) {
end = end + by;
}
}
# A reference struct holding a value: the span is part of the line's object.
struct Line {
var text: String;
var span: Span;
construct(this.text = "") {}
}
# The parameter is a copy: growing it does not change the caller's span.
grown(span: Span, by: int): Span {
span.grow(by);
return span;
}
main(): int {
var a = Span(2, 5);
var b = a; # a copy
b.grow(10);
if (a.length() != 3 || b.length() != 13) {
return 1;
}
var c = grown(a, 1);
if (a.length() != 3 || c.length() != 4) {
return 2;
}
# Through a reference, the change lands in the object's own span.
var line = Line("hello");
var same = line;
line.span.grow(5);
if (same.span.length() != 5) {
return 3;
}
# An element read from an array is a copy: change it, then store it back.
var spans = Array<Span>();
spans.append(a);
var first = spans[0];
first.grow(1);
spans[0] = first;
if (spans[0].length() != 4) {
return 4;
}
# Values compare field by field.
if (a != Span(2, 5)) {
return 5;
}
return 0;
}
Use a value struct for small types that describe a value rather than an
identity: a span, a point, a color. An enum can be a value too (see
Value enums); Optional and Result are. Keep a
reference struct for anything that is shared and changed through several
names, or that is large.
Declaring one
Put [Value] on its own line above the struct. Everything else is written
as for any struct: fields, constructors, methods, operators, generic
parameters, and extensions.
[Value]
struct Point {
var x: int;
var y: int;
construct(this.x = 0, this.y = 0) {}
}
A variable or field of a value struct type that is not given a value holds
the zero value: every field zero, false, or null.
Copies
var b = a;makesban independent copy. Changingbleavesaas it was.- A parameter is a copy of the argument. A function can change its parameter without the caller seeing it; return the changed value instead.
- A field of a value struct is part of whatever holds it. In a reference
struct,
line.span.grow(5)changes the span insideline’s object, and every reference to that object sees it. - Reading an element of an array, or the result of any call, gives a copy.
To change an element, read it into a variable, change it, and store it
back (
spans[0] = first;).
A field that is itself a reference (a String, an Array) is copied as a
reference, so both copies share that object, as two variables holding the
same String do. Its count is kept like any other reference’s: copying the
value acquires it, and the copy going out of scope releases it. Use
copy for a deep copy.
Methods that change this
In a method, this is the value the method was called on, not a copy, so
a method that assigns a field (end = end + by;), or calls another method
that does, changes that value. The compiler works out which methods change
this from their bodies. Calling one on something that would only change a
copy is an error, because the change would be lost:
main.pep:9:12: error: cannot call mutating method 'grow' on a temporary 'Span'
9 | make().grow(1);
| ^~~~
main.pep:9:5: note: 'make' returns a copy; store it in a variable, change it, and assign it back
9 | make().grow(1);
| ^~~~
The same holds for assigning a field of a call’s result (spans[0].end = 3;)
and for changing a const variable’s value. A closure
made in a method holds a copy of this, so it cannot change this either.
Comparing and hashing
== and != compare two values field by field, and hash combines the
fields’ hashes, unless the struct declares its own == or hash (declare
both together; see Hashing and equality). A value
has no identity, so === and !== are errors on it, and it is never null:
comparing one with __null is an error too.
What a value struct cannot do
Each of these is an error at the code to change:
===,!==, or__nullon a value.__weak<T>or__refcountof a value: there is no object to count.- A
destructblock: nothing destroys a value. Its reference fields are still released. [Shared]: a value struct is thread-safe when its fields are, and needs no atomic counts.- A field whose type is the struct itself, directly or through other value
structs: the value would be infinitely large. Hold it through a reference
struct or an
Arrayinstead. - A parameter or result of an
externfunction: pass a pointer to C instead.
Value structs as trait values
A value struct can declare traits like any
struct. Where it flows into a slot of a trait type, such as an argument of
type Iterator<int>, it is boxed: copied into a new object that the trait
value refers to. The box is a copy, so a mutating method called through the
trait value changes the box and not the original, and two conversions of
one value make two boxes. Copying the trait value shares the box, as for
any trait value.
The runtime’s iterators (ArrayIterator, StringIterator, the map and
set iterators, ChannelIterator, and the map, filter, enumerate and
zip adapters) are value structs, so a for loop over an Array, a
String or any map or set allocates no iterator.
Hashing and equality
HashMap<K, V> and HashSet<K> find a key by hashing it with hash<K>
and comparing it with ==. What those two mean for a type decides which keys
count as the same.
OrderedMap<K, V> finds keys the same way. For maps that keep their keys
in order, see Maps and sets.
| Key type | == | hash<K> |
|---|---|---|
Numbers, bool, char, pointers | the value | the value’s bits |
String | the content | the content |
| A tuple | element by element | the elements’ hashes, combined |
| A value struct | field by field | the fields’ hashes, combined |
An [Equatable] struct or enum | field by field | the fields’ hashes, combined |
| Any other struct | identity (===) | the object’s address |
Struct keys use identity by default
A struct is a reference. Unless it says otherwise, two references are the same key only when they refer to the same object. Another object with equal fields is a different key:
struct Point {
int x;
int y;
construct(this.x = 0, this.y = 0) {}
}
main(): int {
var origin = Point(0, 0);
var visits = HashMap<Point, int>();
visits[origin] = 1;
# A struct key is found by identity: the same object finds its entry...
assert(visits.contains(origin), "the stored object");
# ...but another object with equal fields is a different key.
assert(!visits.contains(Point(0, 0)), "an equal but distinct object");
return 0;
}
Identity is right for objects that stand for one thing: a connection, a node in a graph, a task. It is wrong for a struct that describes a value, such as a point or a record looked up by its fields.
Deriving both with [Equatable]
Mark the struct [Equatable], and the compiler writes both halves from its
fields, so they cannot drift apart. It compares and hashes like a tuple of
its fields: a field of a struct type uses that struct’s == and hash, a
String its content, and a null field equals only null. Mark a field
[Equatable(exclude = true)], or [Equatable(include = false)], to leave it
out of both; a field with neither takes part:
# `[Equatable]` derives `==` and the hash from the fields, so they always
# agree.
[Equatable]
struct Person {
String name;
int id;
# Not part of the identity: two records for the same person may differ.
[Equatable(exclude = true)]
String nickname;
construct(this.name = "", this.id = 0, this.nickname = "") {}
}
# An enum whose cases carry values compares the case and then its values.
[Equatable]
enum Token {
Word(text: String),
Number(value: int),
End,
}
main(): int {
HashSet<Person> people = [Person("Ada", 1, "A"), Person("Ada", 1, "Countess")];
assert(people.count == 1, "equal people are one member");
assert(people.contains(Person("Ada", 1)), "the nickname is ignored");
assert(!people.contains(Person("Ada", 2)), "a different id");
assert(Person("Ada", 1) == Person("Ada", 1, "B"), "== is derived");
var counts = HashMap<Token, int>();
counts[Token.Word("pepper")] = 2;
counts[Token.End] = 1;
assert(counts[Token.Word("pepper")] == 2, "found by case and value");
assert(Token.Word("salt") != Token.Word("pepper"), "different values");
assert(Token.Number(0) != Token.End, "different cases");
return 0;
}
- An enum whose cases carry values can
be
[Equatable]too: two values are equal when they hold the same case and its values are equal. - A generic struct derives both for each specialization, from the fields its type arguments give it.
includeandexcludearebools, and saying both in a way that contradicts (include = true, exclude = true) is an error. On the struct itself,[Equatable]takes no arguments.- An
[Equatable]struct cannot also declare==or overridehash<T>; either is an error. To compare some other way, drop the attribute and declare both by hand (below). - An
Arrayfield compares by its elements but hashes by its address, so it adds nothing to the hash. Equal keys still hash equally; keys that differ only in such a field share a hash, which costs speed. The same goes for anArrayin a tuple. [Equatable]does not apply to a value struct or an enum whose cases carry nothing: they already compare and hash by content.
Keying a struct by its content
To decide yourself what counts as equal, give the struct both halves of the pair:
- an
==operator that says when two keys are the same, and - an override of
hash<T>for the struct (see Overriding a generic function for one type),hash<Point>(value: Point): u64.
struct Point {
int x;
int y;
construct(this.x = 0, this.y = 0) {}
# Two points are equal when their coordinates are...
==(other: Point): bool {
return x == other.x and y == other.y;
}
}
# ...so equal points must hash equally: hash the same fields `==` compares.
# A tuple hashes each element in turn, which combines them.
hash<Point>(value: Point): u64 {
return hash<(int, int)>((value.x, value.y));
}
struct Person {
String name;
int id;
# Not part of the identity: two records for the same person may differ.
String nickname;
construct(this.name = "", this.id = 0, this.nickname = "") {}
==(other: Person): bool {
return id == other.id and name == other.name;
}
}
# A String field hashes by its content, through hash<String>.
hash<Person>(value: Person): u64 {
return hash<(String, int)>((value.name, value.id));
}
main(): int {
var visits = HashMap<Point, int>();
visits[Point(1, 2)] = 1;
visits[Point(1, 2)] = visits[Point(1, 2)] + 1; # the same key
assert(visits.count == 1, "one entry");
assert(visits[Point(1, 2)] == 2, "found by content");
assert(Point(1, 2) != Point(2, 1), "!= is the negation of ==");
HashSet<Person> people = [Person("Ada", 1, "A"), Person("Ada", 1, "Countess")];
assert(people.count == 1, "equal people are one member");
assert(people.contains(Person("Ada", 1)), "the nickname is ignored");
assert(!people.contains(Person("Ada", 2)), "a different id");
return 0;
}
The two must agree: keys that are == must have the same hash. The
simplest way to keep that promise is to hash exactly the fields that ==
compares, by hashing a tuple of them: hash<(int, int)>((value.x, value.y)). A tuple’s hash combines its elements’ hashes, and each element
uses its own type’s hash, so a String field contributes its content.
Unequal keys may share a hash. That costs speed, not correctness.
With == declared, != is its negation, as for any struct. The struct may
also overload == for other types (==(rhs: int)); the map
and set compare keys with the == that takes the struct itself.
Keep these rules in mind:
-
Declare both or neither. With
==but nohash<T>, two equal keys usually hash differently, so a lookup would miss an entry that is there. Withhash<T>but no==, keys would still compare by identity, so an equal object would never find another’s entry. Either is a compile error when the struct is used as aHashMaporHashSetkey:main.pep:5:5: error: 'Point' declares '==' but no 'hash<Point>', so as a HashMap or HashSet key, equal values will not find each other 5 | ==(other: Point): bool { | ^~ note: declare 'hash<Point>(value: Point): u64' hashing the fields '==' compares, or remove '==' and mark 'Point' '[Equatable]' to derive bothThe runtime’s own structs are not checked. An
Arraykey’s==compares elements but its hash is its address, so find it with the sameArrayobject, not an equal one. -
Don’t change a key’s fields while it is in a map or set. The entry stays where its old hash put it, so lookups by its new or its old fields both miss it. Remove it, change it, then insert it again.
-
One override per program.
hash<Point>replaceshash<T>forPointeverywhere, including insideHashMap, so it may be declared in only one file, and that file must declarePointor be in its module (see overriding a generic function). -
The hash is not keyed, so it does not resist keys chosen by an attacker to collide.
Value structs compare and hash by their fields
A value struct has no identity, so == and hash
already compare and hash it field by field. Declare your own pair only to
change what counts as equal, here to ignore case:
# A value struct compares and hashes by its fields with no code at all.
[Value]
struct Cell {
int row;
int column;
construct(this.row = 0, this.column = 0) {}
}
# A value struct may still declare its own pair, here to ignore case.
[Value]
struct Tag {
String text;
construct(this.text = "") {}
==(other: Tag): bool {
return text.lower() == other.text.lower();
}
}
hash<Tag>(value: Tag): u64 {
return hash<String>(value.text.lower());
}
main(): int {
var board = HashMap<Cell, String>();
board[Cell(0, 0)] = "rook";
assert(board[Cell(0, 0)] == "rook", "fields decide");
var tags = HashSet<Tag>();
tags.insert(Tag("Pepper"));
assert(tags.contains(Tag("PEPPER")), "case is ignored");
return 0;
}
Ordering with Comparable
A type whose values sort in one order declares the Comparable trait. It
asks for one method, compare(other), which returns a negative number, zero
or a positive number as the value sorts before, with or after other, and
gives the type the rest:
- the operators
<,<=,>and>=; min(other),max(other)andclamp(low, high)(which panics whenlowsorts afterhigh);- a place as a
Mapkey orSetelement, in that order; - the constraint
T: Comparablein a generic function, whose body can then use all of the above on aT.
struct Version {
var major: int;
var minor: int;
construct(this.major = 0, this.minor = 0) {}
# The one method Comparable asks for.
compare(other: Version): int {
var order = major.compare(other.major);
if (order != 0) {
return order;
}
return minor.compare(other.minor);
}
}
extend Version with Comparable;
# A generic function asks for an order with `T: Comparable`.
largest<T: Comparable>(items: Array<T>): T {
var best = items[0];
for (var item in items) {
best = best.max(item);
}
return best;
}
main(): int {
# `<`, `<=`, `>` and `>=` come from `compare`.
assert(Version(1, 2) < Version(1, 10), "1.2 sorts before 1.10");
assert(Version(2, 0).min(Version(1, 9)).major == 1, "min");
assert(Version(3, 0).clamp(Version(1, 0), Version(2, 0)).major == 2, "clamp");
# Numbers and strings are Comparable too.
assert(largest([3, 9, 2]) == 9, "numbers");
assert(largest(["pear", "apple"]) == "pear", "strings");
assert((15).clamp(0, 10) == 10, "clamp a number");
# A Comparable struct is a Map key, in its order.
var releases = Map<Version, String>();
releases[Version(2, 0)] = "second";
releases[Version(1, 5)] = "first";
assert(releases.first().value().1 == "first", "sorted by compare");
# Sorting takes the order as a comparer.
var versions = [Version(2, 1), Version(1, 5)];
versions.sort((a, b) => a.compare(b));
assert(versions[0].major == 1, "sorted");
return 0;
}
A type that declares its own < (or min, or any of the others) keeps it;
the trait’s version fills in only what the type leaves out.
compare must give a total, consistent order: the same two values always
compare the same way, a.compare(b) has the opposite sign of
b.compare(a), and if a sorts before b and b before c, a sorts
before c. Map, Set and sorting depend on it.
Numbers, char and String
Every number type and char is Comparable by value, and String by
content, the order of String.compare. Their compare returns -1, 0 or 1
(String.compare any negative or positive number).
float and double compare in a total order: NaN sorts after every number
and with every other NaN, and -0.0 sorts with 0.0. Their <, <=, >
and >= keep IEEE behavior, false whenever a NaN takes part, while
compare, clamp and Map keys follow the total order, so a NaN key
cannot break a map.
Not yet
Array.sort()takes a comparer:items.sort((a, b) => a.compare(b))sorts in the elements’ own order. Asort()with no comparer comes once the published toolchain that builds Pepper has the compiler fix this needs.- Tuples and
Optionalare notComparable, andComparableis not derived from a struct’s fields: a struct writes itscompare, asVersiondoes above.
Maps and sets
The standard library has three kinds of map, which differ in the order they
keep their keys in, and a compact set of small integers,
BitSet:
| Type | Order | Lookup, insert, remove | Keys need |
|---|---|---|---|
HashMap<K, V>, HashSet<K> | none | expected O(1) | hash<K> and == (Hashing and equality) |
Map<K, V>, Set<K> | sorted by key | O(log n) | compare<K> (below) |
OrderedMap<K, V> | the order keys were first inserted | expected O(1) | hash<K> and == |
All of them share one API: m[key] reads (a missing key reads as the
default of V, and is not inserted) and m[key] = value writes; insert,
get, try_get (an optional, Empty when the key is absent),
contains, remove, count, is_empty and clear. A for loop visits a
map’s entries as (key, value) tuples and a set’s members one
by one. Adding or removing an entry while a loop iterates the map panics
(“Map modified during iteration”), as it does for Array and HashMap;
changing the value of a key that is present is allowed.
Sorted: Map and Set
Map<K, V> keeps its entries sorted by key, on a red-black tree. Besides
iterating in order, it answers questions about the order:
first()andlast(): the entries with the smallest and largest key.lower_bound(key): the first entry whose key is at leastkey;upper_bound(key): the first whose key is greater.reversed(): iterates from the largest key down.range(from, to): iterates the keys fromfromup to, not including,to.
Each of the four queries returns an optional (key, value) tuple, Empty when
there is no such entry. Set<K> is the same tree without values, and its
queries return an optional member. An array converts to a Set, so an array
literal can initialize one.
main(): int {
var scores = Map<String, int>();
scores["carol"] = 91;
scores["alice"] = 84;
scores["bob"] = 77;
# Entries come out sorted by key.
var names = "";
for (var (name, score) in scores) {
names = names + name + " ";
}
assert(names == "alice bob carol ", "key order");
# Order queries return an optional (key, value) tuple.
assert(scores.first().value().0 == "alice", "smallest key");
assert(scores.lower_bound("b").value().0 == "bob", "first key >= b");
assert(scores.upper_bound("carol").is_empty(), "nothing after carol");
# A key range: from "a" up to, not including, "c".
var in_range = 0;
for (var (name, score) in scores.range("a", "c")) {
in_range += 1;
}
assert(in_range == 2, "alice and bob");
# A Set keeps sorted members; an array converts to one.
Set<int> primes = [7, 2, 5, 3];
var digits = 0;
for (var p in primes.reversed()) {
digits = digits * 10 + p;
}
assert(digits == 7532, "descending");
return 0;
}
How keys are ordered
Map and Set order keys with the generic function compare<K>(a, b),
which returns a negative number, zero or a positive number as a sorts
before, equal to or after b. Two keys that compare equal are the same key.
- Numbers and
charcompare by value, andStringby content (the same order asString.compare). A NaNfloatordoublekey sorts after every number (see Ordering with Comparable). - A
Comparablestruct, or one that declares a<operator, sorts by it: the genericcompareasksa < bandb < a. - Any other struct declares
Comparable, or overridescompare<T>for itself, as a hashed key overrideshash<T>(see Overriding a generic function for one type).
# A struct with a `<` operator sorts by it.
struct Version {
int major;
int minor;
construct(this.major = 0, this.minor = 0) {}
<(other: Version): bool {
return major < other.major or (major == other.major and minor < other.minor);
}
}
# A struct without one overrides `compare<T>` for itself.
struct Point {
int x;
int y;
construct(this.x = 0, this.y = 0) {}
}
compare<Point>(a: Point, b: Point): int {
return a.x != b.x ? a.x - b.x : a.y - b.y;
}
main(): int {
var releases = Map<Version, String>();
releases[Version(1, 10)] = "third";
releases[Version(0, 9)] = "first";
releases[Version(1, 2)] = "second";
assert(releases.first().value().1 == "first", "0.9 sorts first");
assert(releases.last().value().1 == "third", "1.10 sorts last");
var points = Set<Point>();
points.insert(Point(2, 1));
points.insert(Point(1, 5));
# An equal point (compare returns 0) is the same member.
assert(!points.insert(Point(1, 5)), "already a member");
assert(points.first().value().x == 1, "sorted by x");
return 0;
}
The order must be consistent: the same two keys must always compare the
same way, and if a sorts before b and b before c, a sorts before
c. A struct key whose fields change while it is in a map breaks that
promise.
Insertion order: OrderedMap
OrderedMap<K, V> hashes its keys like HashMap, and iterates its entries
in the order their keys were first inserted, like a Python dict. Setting
the value of a key that is present keeps its place; removing a key and
inserting it again moves it to the end. The other entries keep their order
when one is removed.
main(): int {
var config = OrderedMap<String, String>();
config["name"] = "pepper";
config["version"] = "0.2";
config["license"] = "MIT";
# Setting a key that is present keeps its place.
config["name"] = "Pepper";
# Removing a key and adding it again moves it to the end.
config.remove("version");
config["version"] = "0.3";
var keys = "";
for (var (key, value) in config) {
keys = keys + key + " ";
}
assert(keys == "name license version ", "insertion order");
if (config.try_get("license") is Value(license)) {
assert(license == "MIT", "found");
}
assert(config.try_get("author").is_empty(), "absent");
return 0;
}
Use it where the order of the input matters, such as the keys of a configuration file that should be written back as they were read.
Small integers: BitSet
BitSet is a set of non-negative ints that stores one bit per possible
member, so a set whose largest member is m takes about m / 8 bytes
however many members it holds. It suits visited flags, masks, and sieve or
liveness tables over small integers; for sparse or large integers, use a
HashSet<int>.
It has the set API above (insert, contains, remove, count,
is_empty, clear) and iterates its members in ascending order. insert
grows the set as needed and reserve(n) makes room for 0 to n - 1
ahead of time; it never shrinks. Inserting a negative number panics, while
contains and remove of one return false. Changing the set while a loop
iterates it is allowed: the loop sees the members above its position as
they are when it reaches them.
The set operations come in two forms: union, intersection, difference
and symmetric_difference return a new set, and union_with,
intersect_with, difference_with and symmetric_difference_with change
the set in place. is_subset and is_disjoint compare two sets. Two sets
are ==, and hash equally, when they hold the same members whatever their
capacity, so a BitSet works as a HashMap or HashSet key. An array
converts to a BitSet, so an array literal can initialize one.
main(): int {
# A sieve: every number below 50 starts out as a candidate.
var composite = BitSet(50);
var primes = BitSet();
for (var n in 2..50) {
if (composite.contains(n)) {
continue;
}
primes.insert(n);
var multiple = n * n;
while (multiple < 50) {
composite.insert(multiple);
multiple = multiple + n;
}
}
assert(primes.count == 15, "15 primes below 50");
# Members come out in ascending order.
var first = "";
for (var p in primes) {
if (p > 11) {
break;
}
first = $"{first}{p} ";
}
assert(first == "2 3 5 7 11 ", "ascending");
# Set operations, by value or in place.
BitSet odd = [1, 3, 5, 7, 9, 11];
var odd_primes = primes.intersection(odd);
assert(odd_primes.count == 4, "3, 5, 7 and 11");
assert(odd_primes.is_subset(primes), "a subset");
odd.difference_with(primes);
assert(odd == BitSet([1, 9]), "1 and 9 are odd but not prime");
return 0;
}
Queues: Deque and PriorityQueue
Two collections take elements out in an order other than an Array’s:
| Type | Takes out | Push, pop | Elements need |
|---|---|---|---|
Deque<T> | either end | amortized O(1) at both ends | nothing |
PriorityQueue<T> | the greatest element | O(log n) | compare<T>, or a comparer |
An Array can stand in for either, but removing its first element takes
O(n), and keeping it sorted takes O(n) per insert.
Deque
A Deque (a double-ended queue) is a ring buffer that grows when full:
push_back, push_front, pop_back and pop_front add and remove at the
two ends, front() and back() read them, and d[i] reads or writes the
element i places from the front. It is the queue of a breadth-first
search, a sliding window, or an undo history that drops its oldest entry.
# Breadth-first search: a Deque is the queue of nodes to visit.
main(): int {
# Each node's neighbors: 0 - 1 - 2 - 4 and 0 - 3 - 4.
int[][] edges = [[1, 3], [0, 2], [1, 4], [0, 4], [2, 3]];
int[] hops = [0, -1, -1, -1, -1];
Deque<int> queue = [0];
while (!queue.is_empty) {
var node = queue.pop_front();
for (var next in edges[node as ulong]) {
if (hops[next as ulong] >= 0) {
continue;
}
hops[next as ulong] = hops[node as ulong] + 1;
queue.push_back(next);
}
}
assert(hops[4] == 2, "two hops from node 0 to node 4");
# Both ends work: a window of the last three readings.
var window = Deque<int>();
for (var reading in [4, 8, 15, 16, 23]) {
window.push_back(reading);
if (window.count > 3) {
window.pop_front();
}
}
assert(window.front() == 15 and window.back() == 23, "the last three");
assert(window[1] == 16, "indexed from the front");
return 0;
}
An array converts to a Deque, so an array literal can initialize one.
count, is_empty, capacity, reserve and clear work as they do on an
Array. A for loop visits the elements from front to back; changing the
deque while a loop iterates it panics (“Deque modified during iteration”).
Popping, or reading front() or back(), of an empty deque panics, as does
an index past the back.
PriorityQueue
A PriorityQueue is a binary heap: push adds an element in O(log n), pop
removes the greatest in O(log n), and peek() reads the greatest without
removing it. It is the frontier of Dijkstra’s and A* searches, a scheduler’s
run queue, or the k largest values of a stream.
# A scheduler: the job with the earliest deadline runs first.
struct Job {
var name: String;
var deadline: int;
construct(name: String, deadline: int) {
this.name = name;
this.deadline = deadline;
}
}
main(): int {
# The comparer is reversed, so the smallest deadline is the greatest.
var jobs = PriorityQueue<Job>((a, b) => b.deadline.compare(a.deadline));
jobs.push(Job("backup", 30));
jobs.push(Job("email", 5));
jobs.push(Job("report", 12));
assert(jobs.peek().name == "email", "the earliest deadline");
var order = "";
while (!jobs.is_empty) {
order = $"{order}{jobs.pop().name} ";
}
assert(order == "email report backup ", "by deadline");
# Comparable elements need no comparer: the greatest comes out first.
PriorityQueue<int> scores = [72, 95, 88, 61];
assert(scores.pop() == 95, "the top score");
var rest = scores.into_sorted_array();
assert(rest == [61, 72, 88], "the rest, ascending");
return 0;
}
Elements are ordered by compare<T>, so numbers, String, and any
Comparable type need nothing more. To order by something
else, pass a comparer to the constructor: a function that returns a
negative number, zero or a positive number as its first argument sorts
before, with or after its second. For the smallest first, reverse the
comparer, (a, b) => b.compare(a). Elements that sort together come out in
no particular order.
An array converts to a PriorityQueue, and PriorityQueue<T>(items, compare) builds one from an array with a comparer; both take O(n), and copy
the array. into_sorted_array() empties the queue and returns its elements
from smallest to greatest. pop and peek of an empty queue panic.
Neither type is [Shared]: like an Array, each stays on one thread.
Enums and match
An enum is a type with a fixed set of named cases. A value of the
enum holds exactly one of them. Use an enum where you would otherwise pass an
integer code or a string, so the compiler checks that only the listed cases
flow through, and that a match handles each one.
There are two kinds:
- In an enum whose cases carry nothing (
Red,Green), each case is an integer. Such an enum is a value type, likeint. - When a case carries values (
Circle(radius: int)), the enum is a reference type, like a struct. See Cases that carry values.
# An enum names a fixed set of cases; a slot of the enum type holds one.
enum Suit {
Clubs,
Diamonds,
Hearts,
Spades,
}
# A representation sets the stored integer type; a case may give its value.
enum Status: u8 {
Ok,
NotFound = 44,
Failed,
}
struct Card {
Suit suit;
int rank = 1;
}
main(): int {
var card = Card();
assert(card.suit == Suit.Clubs, "a slot starts at the case whose value is 0");
card.suit = Suit.Hearts;
assert(card.suit != Suit.Spades, "cases compare with == and !=");
# 'as' turns a case into its integer; the reverse is not allowed.
assert((Suit.Hearts as int) == 2, "cases number from 0");
assert((Status.Failed as int) == 45, "numbering continues after an explicit value");
assert(sizeof<Status>() == 1, "Status is stored as a u8");
return 0;
}
Declaring an enum
enum Name { ... } lists the cases, separated by commas; a comma after the
last case is optional. Case names are PascalCase, like types (see
naming conventions). A case may share a type’s name
(String(value: String)); a string literal in the enum’s methods is still
a String, and String(...) there calls the case.
Each case is an integer. The first case is 0 and each case after it is one
more than the one before. A case can give its own value with = value, and
numbering continues from there:
enum Status: u8 {
Ok, # 0
NotFound = 44, # 44
Failed, # 45
}
A value must be an integer constant: a literal such as -1 or 0x20, or a
character such as 'y'. Two cases cannot have the same value.
The type after the colon is the representation, the integer type the
enum is stored as. It can be any integer type (u8, short, int, long,
and so on). Without one, the enum is stored as an int. Every case value
must fit in the representation.
A case can carry a [Target] attribute, so a
platform can have cases that others lack. A dropped case does not renumber
the others.
Using enum values
This section is about enums whose cases carry nothing.
Name.Case is a value of the enum type. Such an enum is a value type, like int:
it is copied, not reference counted, and it can be stored in locals, fields,
arrays and module-scope constants, and passed to and returned from functions.
==and!=compare two values of the same enum. Other operators apply only when the enum declares them (see Operators and indexers).- An enum does not mix with integers or with other enums:
int x = Color.RedandColor c = 0are errors, and so is comparing aColorwith aSize. value as int(or any integer type) gives a case’s integer value. The reverse,1 as Color, is an error, since the integer may not name a case; name the case, or usefrom_value.- A slot of an enum type that is not given a value (a field, or a local declared without one) holds the case whose value is 0, or the first case when no case is 0.
sizeof<Name>()is the size of the representation.- An enum works wherever an integer does: as a
HashMapkey orHashSetelement, in anAtomic<Name>, and in anexternfunction’s signature, where C sees the representation (anintunless the enum names another).
Cases that carry values
A case can carry values, declared like parameters: Circle(radius: int).
An enum with such a case is a reference type: each value is an object,
reference counted like a struct.
# Cases that carry values: each case is a constructor, and a `match`
# pattern binds the values of the case it matches.
enum Shape {
Circle(radius: int),
Rect(width: int, height: int),
Square(side: int),
Point,
area(): int {
match (this) {
Circle(r) => { return 3 * r * r; }
Rect(w, h) => { return w * h; }
Square(s) => { return s * s; }
Point => { return 0; }
}
return 0;
}
}
# A generic enum can name itself: a list is empty, or a value and the rest.
enum List<T> {
Empty,
Cons(head: T, tail: List<T>),
}
length(list: List<String>): int {
match (list) {
Empty => { return 0; }
# `_` skips a value.
Cons(_, tail) => { return 1 + length(tail); }
}
return 0;
}
main(): int {
var shapes = Array<Shape>();
shapes.append(Shape.Circle(2));
shapes.append(Shape.Rect(3, 4));
shapes.append(Shape.Point);
var total = 0;
for (var s in shapes) {
total += s.area();
}
assert(total == 24, "12 + 12 + 0");
# Several patterns in one arm bind the same names.
var side = 0;
match (Shape.Square(5)) {
Rect(w, _), Square(w) => { side = w; }
else => {}
}
assert(side == 5, "a square's side");
var names = List<String>.Cons("a", List<String>.Cons("b", List<String>.Empty));
assert(length(names) == 2, "two names");
assert(names.name() == "Cons", "name() spells the case");
return 0;
}
Name.Case(values...)makes a new value, like a constructor call, andName.Casealone is a case that carries nothing. An enum’s values are made only by its cases:Shape(...)is an error.- A case that carries values must be given them:
Shape.Circlewithout(radius)is an error. - The values are fixed once made. There is no way to change them, so a value can be shared freely. To change one, make a new value.
- Each case that carries nothing is a single object that every use shares. It is never freed and costs no allocation, however often it is used.
- A slot of the enum type that is not given a value holds null, as a struct
slot does.
===and!==compare identity, and so does==, unless the enum is[Equatable]. - Mark the enum
[Equatable]to derive==andhashfrom its cases: two values are equal when they hold the same case with equal values, so they work asHashMapandHashSetkeys. - An enum can have type parameters (
enum List<T>), and its cases can hold the enum itself, directly or through other types, so lists and trees are enums. - The enum can be
[Shared]when every value its cases carry is thread-safe (see What threads can share). An enum whose cases carry nothing is stored as an integer and is always thread-safe. It can conform to a trait withextend Name with Trait, like a struct, and its methods can come from anextendblock too. - Cases cannot have explicit values (
= 3), and the enum has no representation type (: u8), since its values are not integers.
Value enums
An enum whose cases carry values can be marked [Value], like a
value struct. Each value is then stored inline in
whatever holds it, and copied when assigned, passed or returned, instead of
being an object. Making one allocates nothing, which suits small enums that
are made often, such as Optional and
Result, which are both value enums.
[Value]
enum Shape {
Circle(radius: int),
Rect(width: int, height: int),
Point,
}
- Its cases share one storage: a value is as large as its largest case, plus
the tag that says which case it holds (
sizeof<Shape>()is 12: the tag, then room forRect’s twoints, whichCircle’sradiusreuses). - A value enum is never null. A slot that is not given a value holds the
first case, with every value it carries zero,
false, or null: an unsetShapeisCircle(0). ==compares the cases and their values;===,!==and__nullare errors.- Its cases cannot hold the enum itself, since the value would be infinitely large. Use a reference enum for lists and trees.
- Everything else a value struct cannot do applies too, and so does boxing when it flows into a trait value.
Methods
An enum can declare methods after its cases. Inside a method, this is the
value the method was called on, and a bare call of another method of the
enum (next()) calls it on this. A static method is called on the enum
itself (Light.first()). A method can be generic (choose<T>), with its
type arguments inferred or written out (light.choose<String>(...)).
# An enum can declare methods after its cases; `this` is the value.
enum Light: u8 {
Red,
Amber,
Green,
may_go(): bool {
return this == Light.Green;
}
next(): Light {
match (this) {
Red => { return Light.Green; }
Amber => { return Light.Red; }
Green => { return Light.Amber; }
}
return this;
}
# A bare call of another method acts on `this`.
after_two(): Light {
return next().next();
}
# A method can be generic, like any function.
choose<T>(stop: T, go: T): T {
return may_go() ? go : stop;
}
static first(): Light {
return Light.Red;
}
}
main(): int {
var light = Light.first();
assert(!light.may_go(), "red means stop");
assert(light.next().may_go(), "green follows red");
assert(light.after_two() == Light.Amber, "then amber");
assert(light.choose("wait", "walk") == "wait" && Light.Green.choose(0, 1) == 1, "a generic method");
# Every enum has name() and from_value().
assert(light.name() == "Red", "name() spells the case");
var found = Light.from_value(2);
assert(found.is_ok() and found.value() == Light.Green, "2 is Green");
assert(!Light.from_value(7).is_ok(), "no case has the value 7");
# Enums key hash maps and fill atomics, like integers.
var waits = HashMap<Light, int>();
waits[Light.Red] = 30;
assert(waits[Light.Red] == 30, "an enum is a hash key");
return 0;
}
For an enum whose cases carry nothing, this is passed by value, like an
int: a method cannot change the value it was called on. Return a new case
instead, as next() does. Such an enum can also declare a trait with
extend Light with Describe { ... }, which makes it meet the constraint
T: Describe (see
numbers, bool and enums);
it is still never a trait value. For an enum whose cases carry values, this is
the object, as in a struct’s method; its values still cannot change.
Operators and indexers
An enum can declare operators and an
indexer the way a struct does: a method named after
the operator (<(other: Level): bool) or []. For an enum whose cases
carry nothing, a < b calls Level.< with a as this.
# An enum can declare operators and an indexer, as a struct does.
enum Level: u8 {
Low,
Mid,
High,
# `a < b` calls `Level.<`: the cases rank in order.
<(other: Level): bool {
return (this as int) < (other as int);
}
# `level + 1` moves up a level, stopping at the top.
+(steps: int): Level {
var next = Level.from_value((this as int + steps) as u8);
return next.is_ok() ? next.value() : Level.High;
}
# `-level` mirrors it.
-(): Level {
return match (this) {
Low => Level.High,
Mid => Level.Mid,
High => Level.Low,
};
}
# `level[i]` reads, like any indexer.
[](bit: int): bool {
return ((this as int) >> bit) & 1 == 1;
}
}
main(): int {
var level = Level.Low;
assert(level < Level.Mid && !(Level.High < level), "cases rank in order");
assert(level + 1 == Level.Mid && level + 5 == Level.High, "a step up");
assert(-level == Level.High, "mirrored");
assert(Level.Mid[0] && !Level.Mid[1] && Level.High[1], "bits");
return 0;
}
Its values never change and it already compares with == and !=, so
such an enum cannot declare ==, !=, a compound assignment (+=), an
indexer that assigns ([](index, value)) or the truth test !!, and its
operators are not static.
Properties
An enum can also declare computed properties after its
cases, var name: T { get { ... } }, which are read like fields
(size.ounces). Inside the enum, a bare property name reads it on this.
# An enum can declare computed properties after its cases, read like
# fields: `size.ounces`, not `size.ounces()`.
enum Size {
Small,
Medium,
Large,
var ounces: int {
get {
return match (this) {
Small => 8,
Medium => 12,
Large => 16,
};
}
}
# Inside the enum, a bare property name reads it on `this`.
var cups: int {
get { return ounces / 8; }
}
}
# The same works for an enum whose cases carry values.
enum Shape {
Circle(radius: int),
Square(side: int),
var area: int {
get {
return match (this) {
Circle(r) => 3 * r * r,
Square(s) => s * s,
};
}
}
}
main(): int {
var size = Size.Medium;
assert(size.ounces == 12 && Size.Large.cups == 2, "value enum properties");
assert(Shape.Square(4).area == 16 && Shape.Circle(2).area == 12, "properties that read the payload");
return 0;
}
An enum value has nowhere to keep anything and never changes, so an enum’s
property has a get with a body that does not use field, and no set:
main.pep:6:9: error: property 'n' of enum 'E' must be computed: give 'get' a body that does not use 'field'
main.pep:6:38: error: property 'n' of enum 'E' cannot have a 'set' accessor: enum values are immutable
Built-in methods
Every enum has these methods without declaring them, and cannot declare methods of the same names:
| Method | What it does |
|---|---|
value.name() | Returns the case’s name as written ("Red"), a String. Every enum has it. |
Name.from_value(n) | Returns a Result<Name>: Ok with the case whose value is n, or an Err when no case has it. n has the enum’s representation type. Only enums whose cases carry nothing have it. |
from_value is the checked way back from an integer (from a file, a C
library or the network) to a case, since as only goes the other way.
match
match (value) { ... } runs the arm for the case that value holds:
# 'match' runs the arm of the case a value holds, and must handle every case.
enum Light {
Red,
Amber,
Green,
}
next(light: Light): Light {
match (light) {
Red => { return Light.Green; }
Amber => { return Light.Red; }
Green => { return Light.Amber; }
}
return light;
}
may_go(light: Light): bool {
match (light) {
Green => { return true; }
else => { return false; }
}
return false;
}
wait_seconds(light: Light): int {
match (light) {
Red, Amber => { return 30; }
Light.Green => { return 0; }
}
return 0;
}
main(): int {
var light = Light.Red;
var changes = 0;
while (!may_go(light)) {
light = next(light);
changes += 1;
}
assert(changes == 1, "red turns green");
assert(wait_seconds(Light.Amber) == 30, "one arm handles two cases");
assert(wait_seconds(light) == 0, "a pattern may name the enum");
return 0;
}
- Each arm is one or more patterns, separated by commas, then
=>and a block. A pattern is a case name (Red) or the case qualified by its enum (Light.Red). - A pattern for a case that carries values can bind them:
Rect(w, h)declareswandhin its arm, holding the case’s values in order. The bindings areconst._skips a value. A pattern lists all of its case’s values or, written without parentheses (Rect), none. Each value can also be a pattern of its own (see Nested patterns). - Patterns that share an arm must bind the same names, with the same types
(
Rect(w, _), Square(w)). - An
else => { ... }arm, last, handles every case the other arms do not. - The arms must handle every case. A missing case is an error that names
it, located at
match; the language server offers a quick fix that adds an empty arm for each missing case. Anelsethat can never run, because the other arms handle every case, is a warning. - Handling a case twice is an error.
- In a
matchstatement,break,continueandreturninside an arm act on the enclosing loop or function, as they would inside anif.
The value is evaluated once, before any arm runs. A match on a null value
of an enum whose cases carry values panics with match on a null 'Name'; a
value enum is never null.
match expressions, is, and literal patterns
# `match` produces a value, `is` tests one case, and a `match` on an
# integer, character, `bool` or `String` compares with literals.
enum Shape {
Circle(radius: int),
Rect(width: int, height: int),
Point,
}
enum List {
Empty,
Cons(head: int, tail: List),
}
area(s: Shape): int {
return match (s) {
Circle(r) => 3 * r * r,
Rect(w, h) => w * h,
Point => 0,
};
}
keyword(word: String): int {
return match (word) {
"import" => 1,
"export", "extern" => 2,
else => 0,
};
}
# The arms' values are numbers of different types (`0` is an `int`,
# `length` a `ulong`), so the `match` has their common type, `ulong`, and
# converts to the `int` this function returns.
length_of(word: String): int {
return match (word) {
"" => 0,
else => word.length,
};
}
main(): int {
var total = area(Shape.Rect(2, 3)) + keyword("extern");
# `is` binds only in the block of the `if` or `while` it is the
# condition of.
var s = Shape.Circle(1);
if (s is Circle(r)) {
total += r;
}
var list = List.Cons(1, List.Cons(2, List.Empty));
while (list is Cons(head, tail)) {
total += head;
list = tail;
}
var point = Shape.Point is Point;
if (point && !(s is Rect)) {
total += 100;
}
# A `match` on an integer needs an `else`: literals cannot list every
# value.
var size = match (total) {
0 => "none",
1, 2, 3 => "few",
else => "many",
};
assert(size == "many", "total is past three");
assert(length_of("four") == 4 && length_of("") == 0, "lengths convert");
assert(total == 6 + 2 + 1 + 3 + 100, "every step adds up");
return 0;
}
- A
matchthat produces a value can go wherever a value can: aftervar x =,return, in an argument. Each arm ispatterns => value, and arms are separated by commas (one after the last is allowed). It follows the rules above: a pattern binds names for its arm’s value, and the arms must handle every case unless there is anelse. The value has the first arm’s type, and every other arm’s value must convert to it. When the arms are numbers of different types, the value has their common type, as C’s? :does (intandlonggivelong), and converts from it wherever it goes (0or aulonglength, returned as anint). A closure in an arm’s value cannot use a name the arm binds; use amatchstatement there. value is Patternis abool: whethervalueholds that case (d is Line,!(d is Text)). It takes any one pattern. When it is the whole condition of aniforwhile, its bindings are declared in that block only:if (d is Text(t)) { ... }readstinside the braces, andwhile (list is Cons(head, tail)) { ... }examineslistagain on every iteration. Anywhere else,iscannot bind names.isis a keyword only between a value and a pattern, so it stays usable as a name (token.is(text)).- A trait value is matched by type rather than by case:
Circle(c)tests for the structCircleand binds the value as one. See Converting a trait value back. - Literal patterns: a
matchon an integer, a character, aboolor aStringcompares with literals (1,-1,'a',true,"import"), using==. Literals cannot list every value, so such amatchneeds anelsearm, except on aboolthat handles bothtrueandfalse. Matching the same literal twice is an error. A match on an enum takes its case names, not literals, and a match oninttakes literals, not names (except constants).
Nested patterns
The values in a case’s parentheses are patterns, as a tuple pattern’s elements are, so patterns nest to any depth:
# Patterns nest: the values a case carries can be matched by patterns of
# their own, as tuple elements can.
enum Doc {
Text(text: String),
Line,
Pair(left: Doc, right: Doc),
}
describe(found: Optional<Doc>): String {
return match (found) {
Empty => "nothing",
# A literal compares the value: only an empty `Text`.
Value(Text("")) => "empty text",
# A case of the value's enum, binding its own values.
Value(Text(t)) => t,
Value(Line) => "a line",
# Deeper still, with `_` for a value that does not matter.
Value(Pair(Line, _)) => "a pair that starts with a line",
Value(Pair) => "a pair",
};
}
main(): int {
assert(describe(Optional<Doc>.Empty) == "nothing", "no document");
assert(describe(Optional<Doc>.Value(Doc.Text(""))) == "empty text", "a literal value");
assert(describe(Optional<Doc>.Value(Doc.Text("hi"))) == "hi", "a bound value");
assert(
describe(Optional<Doc>.Value(Doc.Pair(Doc.Line, Doc.Text("x")))) == "a pair that starts with a line",
"two levels deep"
);
# A bare name binds the value, unless the value's enum has a case of
# that name: here `Empty` is `Optional<int>`'s case.
var nested = Optional<Optional<int>>.Value(Optional<int>.Empty);
var depth = match (nested) {
Empty => 0,
Value(Empty) => 1,
Value(Value(n)) => 2 + n,
};
assert(depth == 1, "the inner Optional is empty");
# Ranges and guards work inside too, and so does `is`.
var count = Optional<int>.Value(7);
var size = match (count) {
Value(0) => "none",
Value(1..10) => "a few",
Value(n) if n < 0 => "negative",
Value(_) => "many",
Empty => "unknown",
};
assert(size == "a few", "a range inside a case");
var pair = Doc.Pair(Doc.Text("left"), Doc.Line);
if (pair is Pair(Text(word), Line)) {
assert(word == "left", "is binds nested names");
}
return 0;
}
- Each value takes
_, a name, a case of its enum (with patterns of its own values:Value(Pair(Line, _))), a literal, a range or constant, or a tuple pattern. - A bare name binds the value, unless the value’s enum has a case of that
name, which it then matches (
Value(Empty)), or it names a module-scope constant. A name repeated in one pattern (Pair(x, x)) must be a case. - A nested pattern handles part of its case: the arms must still handle
every value. A case that only nested patterns handle is reported with the
value they miss (
match on 'Optional<Doc>' does not handle 'Value(Line)'), and a pattern whose values the earlier arms already match is an error. - Patterns that share an arm bind the same names, each at the same place:
Pair(Text(t), Line), Pair(Text(t), Pair)bindstfrom either, butPair(Text(t), _), Pair(_, Text(t))is an error. - A nested test reads a value only once the case around it matched, so the tests stop at the first part that does not match.
Ranges and constants
A match on an integer or a character can compare with a range of
values, and a match on an integer, a character or a bool with a
module-scope constant:
# Range and constant patterns: a `match` on an integer or a character
# compares with ranges (`1..9`, `'a'..>'z'`) and module-scope constants.
const LIMIT = 100;
size(n: int): String {
return match (n) {
0 => "none",
# `1..10` stops before 10; `10..>99` includes 99.
1..10 => "a few",
10..>99 => "many",
LIMIT => "the limit",
else => "out of range",
};
}
kind(c: char): String {
return match (c) {
'a'..>'z', 'A'..>'Z' => "letter",
'0'..>'9' => "digit",
else => "other",
};
}
# Ranges that cover every value of the type need no `else`.
half(b: u8): int {
return match (b) {
0..128 => 0,
128..>255 => 1,
};
}
main(): int {
assert(size(0) == "none" && size(9) == "a few" && size(10) == "many", "ranges");
assert(size(100) == "the limit" && size(-1) == "out of range", "a constant, then else");
assert(kind('q') == "letter" && kind('7') == "digit" && kind('!') == "other", "character ranges");
u8 high = 200;
assert(half(3) == 0 && half(high) == 1, "every u8 is handled");
# In a tuple pattern, a range or a constant compares its element.
var pair = (LIMIT, true);
var which = match (pair) {
(0..LIMIT, _) => 1,
(LIMIT, true) => 2,
(_, _) => 3,
};
assert(which == 2, "the constant matches the first element");
return 0;
}
start..endmatches fromstartup to but not includingend, andstart..>endincludesend, as ranges do elsewhere. Each bound is a literal (-1,'z') or a constant (MIN..MAX). Ranges match integers and characters only, notboolorString.- A name that is a module-scope constant (
const LIMIT = 100;, declared in the file or imported, orlimits.MAXthrough a module) compares the value with it, like a literal. A local variable is not a pattern; compare with it in a guard instead (n if n == limit). - Inside a tuple pattern, a range or a
constant compares its element (
(0..LIMIT, _)), and any other bare name binds it, as before. - When every bound is a literal, the compiler knows which values a pattern
matches: a pattern whose values the earlier arms already match is an
error (
2..>5after1..9), and amatchwhose ranges and literals cover every value of its type (0..128and128..>255for au8) needs noelse. A pattern with a constant bound covers nothing for this check. - A
matchon an enum takes its cases, not ranges or constants, even a constant holding one of its cases.
Guards
An arm can add a guard after its patterns, if condition: the arm runs
only when one of its patterns matches and the condition holds. Otherwise
the next arm is tried, as if this one had not matched.
# A guard (`if condition`) after an arm's patterns: the arm runs only when a
# pattern matches and the guard holds; otherwise the next arm is tried.
enum Shape {
Circle(radius: int),
Rect(width: int, height: int),
Point,
}
describe(s: Shape): String {
return match (s) {
# The guard reads the names its patterns bind.
Circle(r) if r > 10 => "a large circle",
Circle => "a circle",
Rect(w, h) if w == h => "a square",
Rect => "a rectangle",
Point => "a point",
};
}
# A guarded arm handles nothing for the check that every value is handled,
# since its guard may fail: the `bool` match below still needs `false`.
toll(heavy: bool, axles: int): int {
match (heavy) {
true if axles > 4 => {
return 30;
}
true => {
return 20;
}
false => {
return 10;
}
}
}
main(): int {
assert(describe(Shape.Circle(20)) == "a large circle", "the guard holds");
assert(describe(Shape.Circle(2)) == "a circle", "the guard fails, the next arm runs");
assert(describe(Shape.Rect(3, 3)) == "a square", "equal sides");
assert(describe(Shape.Rect(3, 4)) == "a rectangle", "unequal sides");
assert(toll(true, 6) + toll(true, 2) + toll(false, 2) == 60, "each arm runs once");
# Guards work on tuples too.
var pair = (5, true);
var kind = match (pair) {
(n, true) if n > 3 => 1,
(_, true) => 2,
(_, false) => 3,
};
assert(kind == 1, "the first arm's guard holds");
return 0;
}
- The guard reads the names the arm’s patterns bind (
Circle(r) if r > 10). With several patterns (Word(s), Quoted(s) if s.length == 0), it applies to the arm, whichever pattern matched. - Guards work in
matchstatements andmatchexpressions, over enums, literals and tuples.istakes no guard: writex is P && condition, or test the condition inside theif. - A guarded arm handles nothing when the compiler checks that every case is
handled, since its guard may fail: a
matchwhose only arm forCirclehas a guard does not handleCircle, and a later unguarded arm (orelse) must. Anelseafter guarded arms is not unreachable. - A guarded arm for a case an earlier unguarded arm already handles is an error, like any case handled twice. Two guarded arms may name the same case.
- A closure in a guard cannot use a name the arm binds.
Current limits
- An enum whose cases carry nothing cannot be generic, by design: its value
is a plain integer, so a type parameter would have nothing to describe.
An enum whose cases carry values can be (
enum List<T>). - An enum whose cases carry values compares by identity with
==, as a struct does, unless it is a[Value]enum, is[Equatable], or declares==(with a matchinghash<T>to be a key). - A constant is a pattern only when it is declared at module scope.
- A
matchcannot examine floating-point values. - The methods of an enum whose cases carry nothing have no value form:
c.namewithout the call is an error (“‘name’ is a method of ‘Color’: call it as ‘name()’”), where a struct’sp.normis a bound method. Wrap the call in a closure instead,() => c.name(). - An enum whose cases carry nothing is extended only with a trait
(
extend Color with Describe { ... }); declare its other methods inside it. It is never a trait value, since a trait value is a reference to an object, which such an enum’s value is not.
Memory and reference counting
Pepper frees memory for you, without a garbage collector and without ownership annotations. Every struct object keeps a count of the references to it, and the object is destroyed the moment that count drops to zero. Because the rules are simple and the same everywhere, you can always tell when an object will be destroyed and its destructor will run.
If you know C++, the whole model fits in one sentence: every struct value
behaves exactly like a std::shared_ptr<T> passed by value.
Values and references
Numbers, bool, and the other primitive types are plain values: assigning
one copies it, as in C. So is a tuple, whose elements are
copied the way each element would be on its own, and so is a struct or enum
marked [Value], such as Optional<T> and Result<T>
(see value enums).
Everything else, including your own structs, String, Array, HashMap, and
function values, is a reference to an object on the
heap. Assigning a reference, passing it to a function, or storing it in a
field or an array shares the object. Nothing is copied.
struct Account {
var balance: int = 0;
}
# The parameter is another reference to the caller's object, so changes to
# its fields are visible to the caller.
deposit(account: Account, amount: int) {
account.balance += amount;
}
# Rebinding the parameter itself only changes this function's reference.
replace(account: Account) {
account = Account();
account.balance = 999;
}
main(): int {
var mine = Account();
var yours = mine; # shares the object; nothing is copied
yours.balance = 50;
assert(mine.balance == 50, "assignment shares the object");
deposit(mine, 25);
assert(mine.balance == 75, "a function can change the shared object");
replace(mine);
assert(mine.balance == 75, "rebinding a parameter does not affect the caller");
# To get an independent object, make a new one and copy the fields.
var copy = Account();
copy.balance = mine.balance;
copy.balance = 0;
assert(mine.balance == 75, "the copy is a separate object");
return 0;
}
var yours = mine;makes a second reference to the sameAccount. Changes through either name are visible through both.- A parameter is one more reference to the caller’s object. The function can change the object’s fields, but assigning a new object to the parameter only changes the function’s own reference.
- To get an independent object, create a new one. Pepper does not copy
structs for you; some library types offer a method for it, such as
Array’sclone().
A struct-typed variable or field that has not been given a value, such as
var next: Account;, is null: it refers to no object. Reading a field or
calling a method through a null reference crashes the program. Use === or
!== to check whether two references name the same object, or whether one is
null (next === __null). A struct is not a condition by itself, so
if (next) is an error unless the struct declares the truth test !! (see
Truth tests and conditions). For a
value that may be absent, prefer Optional<T>, whose type
says so.
When objects are destroyed
A reference is held by whatever stores it: a local variable, a parameter, a field, an element of a container, or a temporary value in the middle of an expression. Each of those releases its reference exactly once, when it goes away. When the last reference is released, the object is destroyed: its destructor runs, and then its fields release their own references, which may destroy further objects in turn.
import async;
# A module-scope counter: const, and changed through an Atomic.
const destroyed = async.Atomic<int>(0);
struct Tracked {
var name: String;
construct(this.name = "") {}
destruct {
destroyed.fetch_add(1);
}
}
struct Holder {
var item: Tracked;
}
make(name: String): Tracked {
var t = Tracked(name);
return t; # the caller receives the object
}
main(): int {
# 1. A local is released when its scope ends.
{
var a = Tracked("a");
}
assert(destroyed.load() == 1, "a was destroyed at the end of its block");
# 2. While another reference exists, the object stays alive.
var b = Tracked("b");
var also_b = b;
b = Tracked("b2"); # b lets go of the first object...
assert(destroyed.load() == 1, "...but also_b still holds it");
also_b = b; # the last reference moves away
assert(destroyed.load() == 2, "the first object is destroyed now");
# 3. Fields and containers hold references too.
var holder = Holder();
{
var c = Tracked("c");
holder.item = c;
}
assert(destroyed.load() == 2, "holder.item keeps c alive after its scope");
holder = Holder(); # the old Holder dies and releases c
assert(destroyed.load() == 3, "releasing the holder released its field");
var list = Array<Tracked>();
list.append(Tracked("d"));
assert(destroyed.load() == 3, "the array holds d");
list.clear();
assert(destroyed.load() == 4, "clearing the array released d");
# 4. Returned objects belong to the caller; unused results are
# destroyed right away.
var e = make("e");
assert(destroyed.load() == 4, "e is alive in main");
make("temporary");
assert(destroyed.load() == 5, "a result nobody keeps is destroyed immediately");
return 0;
}
Concretely, a reference is released when:
- A local variable goes out of scope, at the closing brace of the block that declared it, or when the function returns.
- A variable or field is overwritten.
b = Tracked("b2");releases the objectbheld before, after taking the new one. Assigning a variable to itself (a = a) changes nothing. - A parameter’s function returns. The function held its parameters for as long as it ran.
- The object that holds a field is destroyed, or a container removes an
element (
clear(), overwritinglist[i], and so on).remove_at()andpop()are the exception: they hand the container’s reference to the caller, sovar x = list.remove_at(0);leaves the count unchanged. - A temporary is no longer needed. A result that is not stored anywhere,
like the one from
make("temporary");, is released at the end of the statement. The same goes for a call result passed straight to another function, and for the object inmake().name.
Returning a local hands its reference to the caller, so the object survives
the function. Calling a method on an object does not add a reference to it:
inside the method, this refers to the caller’s object.
Cycles leak
Reference counting cannot free a cycle: objects that refer to each other, directly or through a chain, keep each other’s counts above zero even after nothing else refers to them. Their destructors never run and their memory is never freed.
import async;
# A module-scope counter: const, and changed through an Atomic.
const destroyed = async.Atomic<int>(0);
struct Person {
var name: String;
var friends: Array<Person>;
construct(this.name = "") {
friends = Array<Person>();
}
destruct {
destroyed.fetch_add(1);
}
}
main(): int {
# Two people who list each other as friends form a cycle: each keeps
# the other alive, so neither is ever destroyed.
{
var ann = Person("Ann");
var bob = Person("Bob");
ann.friends.append(bob);
bob.friends.append(ann);
}
assert(destroyed.load() == 0, "the cycle leaked both objects");
# Break the cycle before letting go, and both are destroyed.
{
var cat = Person("Cat");
var dan = Person("Dan");
cat.friends.append(dan);
dan.friends.append(cat);
dan.friends.clear(); # Dan no longer holds Cat
}
assert(destroyed.load() == 2, "without the cycle, both were destroyed");
return 0;
}
To avoid a cycle, make one direction of it a weak reference (next section), or break the cycle yourself before the last outside reference goes away: clear the container, or overwrite the field, that closes the loop.
A closure holds a reference to every struct it
captures, including this, so a closure stored inside an object it captures
closes a cycle too. The compiler reports the direct case, a method storing a
closure that captures this in a field of this.
Weak references
A Weak<T> points at an object without keeping it
alive. Use it for back pointers: a child’s parent, a document’s folder, an
observer’s subject. The owner holds strong references down the tree, and
everything that points back up holds a Weak<T>, so there is no cycle.
import async;
# A module-scope counter: const, and changed through an Atomic.
const destroyed = async.Atomic<int>(0);
struct Folder {
var name: String;
var files: Array<Document>;
construct(this.name = "") {
files = Array<Document>();
}
destruct {
destroyed.fetch_add(1);
}
}
struct Document {
var title: String;
# A weak back pointer: the folder owns its documents, not the other way
# round, so there is no cycle.
var folder: Weak<Folder>;
construct(String title, Folder folder) {
this.title = title;
this.folder = Weak<Folder>(folder);
}
destruct {
destroyed.fetch_add(1);
}
String path() {
with (var f = folder) {
return f.name + "/" + title;
} else {
return "(deleted)/" + title;
}
return "";
}
}
main(): int {
var kept = Document("draft", Folder("none"));
{
var home = Folder("home");
var notes = Document("notes", home);
home.files.append(notes);
assert(notes.path() == "home/notes", "the folder is alive");
kept = notes;
}
# The folder went away with `home`; the weak pointer did not keep it.
assert(kept.path() == "(deleted)/notes", "the folder is gone");
assert(kept.folder.expired(), "and the weak reference knows it");
return 0;
}
Weak<T>(object)makes the weak reference. It does not change the object’s reference count.- A
withblock reaches the object:with (var f = folder) { ... }bindsfto an ordinary strong reference for the length of the block. When the object has already been destroyed, the body is skipped and theelseblock runs. See Scoped blocks withwith. expired()reports whether the object has been destroyed.Tmay be a trait:Weak<Shape>upgrades to aShape(see Weak references and atomics).- The object is destroyed on time. When its last strong reference goes, its destructor runs and its fields are released, even if weak references remain. Only the object’s small header stays allocated until the last weak reference is gone.
In C++ terms, a struct value is a std::shared_ptr<T> and Weak<T> wraps
the built-in __weak<T>, a std::weak_ptr<T>: Weak<T>(object) is
weak_ptr(object), and entering it with with is lock(). Inside a
constructor, this already holds the object’s first reference, like the
shared_ptr that make_shared fills in: a constructor may pass this to a
function or store it, and a weak reference made from it upgrades, without
the object being destroyed when those copies are dropped.
References and threads
Reference counts are plain, fast increments, so a struct’s objects must stay
on one thread. A struct declared [Shared] has atomic counts instead, and
its references can be copied and dropped on any thread. The compiler requires
such a struct’s fields to be const and thread-safe. See
What threads can share.
Function values follow the same split: a plain function type such as
() => void has plain counts and stays on one thread, and a
shared function type
such as shared () => void has atomic counts.
Copying a value with copy
copy(value) makes a copy of a value that shares no object that is not
thread-safe with the original, so the copy can go to another thread: pass
copy(x) to a channel to send a value you keep
using.
var names = Array<String>();
names.append("ada");
var copied = copy(names);
copied[0].append(" lovelace"); # names[0] is still "ada"
- Every struct object the value reaches is copied once. An object referenced twice is copied once and stays shared inside the copy, and a cycle becomes the same cycle among the copies.
- Thread-safe objects are not copied: a
[Shared]struct or asharedfunction value in the original is the same object in the copy. For a thread-safe value,copyreturns the value itself. - A weak reference to an object that was copied points at its copy. A weak reference to an object outside the copied value becomes null.
- A struct is copied field by field, without running its constructor.
A struct with a destructor or a raw pointer (__ptr<T>) field is not
trivially copyable: copying its fields would share what it owns, such as
a buffer it frees. Such a struct says how to copy itself by implementing
Copyable<T>, with a copy(): T method that builds the copy and copies each
value it holds with copy(value). String, Array, HashMap and
HashSet implement it. String.copy() also copies a single
String; it replaces the deprecated clone(). Array.clone() is unchanged: it
copies the Array but shares its elements.
# A struct with a destructor is not trivially copyable, so it implements
# `Copyable<T>` to say how `copy` copies it.
struct Buffer {
__ptr<u8> data;
ulong size;
String label;
construct(size: ulong) {
data = _pep_malloc(size);
this.size = size;
label = "buffer";
}
destruct {
_pep_free(data);
}
copy(): Buffer {
var made = Buffer(size);
var i: ulong = 0;
while (i < size) {
made.data[i] = data[i];
i = i + 1;
}
# The free function `copy`: it continues the running copy.
made.label = copy(label);
return made;
}
}
extend Buffer with Copyable<Buffer>;
struct Message {
Buffer body;
Buffer again;
}
main(): int {
var message = Message();
message.body = Buffer(4);
message.body.data[0] = 9;
message.again = message.body;
var copied = copy(message);
copied.body.data[0] = 1;
copied.body.label.append(" (copy)");
assert(message.body.data[0] == 9, "the original buffer is unchanged");
assert(message.body.label == "buffer", "the original label is unchanged");
# The buffer appears twice, so its copy is one object.
assert(copied.again === copied.body, "one copy of the shared buffer");
return 0;
}
Inside copy(), the call copy(label) is the free function, not the
method: a bare call in a method names the struct’s own method only when one
by that name takes those arguments. While a copy runs, copy(value)
continues it rather than starting another, so an object the outer copy
already made is reused and sharing is kept. That is why a Copyable value
is copied with copy(x): calling x.copy() directly still works, but each
copy(value) inside it is then a copy of its own. A value whose contents can
lead back to it calls register_copy(this, made) before copying them, so
the cycle finds the copy being built.
The copy(copies: Copies): T form of Copyable<T>, which took the copy’s
context as an argument and copied with copies.copy(value), still works in
this release, with a warning at Copies.
Copying a value that reaches a struct that is not trivially copyable and does
not implement Copyable<T> is an error at the copy call. It names the
struct, shows each field on the way to it, says why it cannot be copied, and
shows a Copyable<T> to fill in (the editor offers to insert it):
main.pep:14:13: error: cannot copy 'Log' because it contains 'Handle', which is not trivially copyable
14 | var c = copy(log);
| ^~~~
main.pep:9:5: note: 'Log' reaches it through field 'out'
9 | Handle out;
| ^~~~~~
main.pep:1:8: note: 'Handle' has a destructor, so copying its fields would release what it owns twice
1 | struct Handle {
| ^~~~~~
note: implement 'Copyable<Handle>' to say how to copy it:
extend Handle with Copyable<Handle> {
copy(): Handle {
# Build the copy; copy each struct field with copy(field).
panic("copying 'Handle' is not implemented");
return this;
}
}
Some types cannot be copied at all: an io.File owns an operating-system
handle, so the error says to send its path instead. A plain function value
cannot be copied, because it may capture anything; declare it shared.
Copying trait values
copy of a trait value copies
the struct behind it, as copy of that struct would, and returns the copy
as the same trait type. Sharing works across the boundary: a struct reached
twice, through a trait value or directly, is copied once.
# `copy` of a trait value copies the struct behind it, whichever it is.
trait Shape {
area(): int;
grow();
}
struct Square {
var side: int = 1;
construct(this.side = 1) {}
}
extend Square with Shape {
area(): int { return side * side; }
grow() { side = side + 1; }
}
struct Rect {
var width: int = 1;
var height: int = 1;
construct(this.width = 1, this.height = 1) {}
}
extend Rect with Shape {
area(): int { return width * height; }
grow() { width = width + 1; }
}
main(): int {
var shapes = Array<Shape>();
var square = Square(2);
shapes.append(square);
shapes.append(Rect(2, 3));
shapes.append(square);
var copied = copy(shapes);
for (var shape in copied) {
shape.grow();
}
assert(shapes[0].area() == 4, "the original Square is unchanged");
assert(shapes[1].area() == 6, "the original Rect is unchanged");
# The Square appears twice, so its copy is one object, grown twice.
assert(copied[0] == copied[2], "one copy of the shared Square");
assert(copied[0].area() == 16, "the copied Square grew twice");
assert(copied[1].area() == 9, "the copied Rect grew once");
return 0;
}
Which struct a trait value holds is known only when the program runs, so
the compiler cannot check that it can be copied. Copying a trait value that
holds a struct that cannot be copied (one with a destructor and no
Copyable<T>, an io.File, a MutexGuard) panics:
cannot copy the 'Owner' behind a trait value, which is not trivially copyable
Handing a reference over with move
move(x) gives up x’s reference: the call returns the same object, and
x is left null. Nothing is copied and no count changes. It is how a value
built in a local variable goes to a channel, a
Mutex or another [Move] parameter,
which takes a new value, without the cost of copy(x):
var lines = Array<String>();
lines.append("first");
var log = async.Mutex<Array<String>>(move(lines)); # lines is now null
move is an ordinary function from the prelude (runtime/copy.pep), but
the compiler checks every call:
-
Only a local variable can be moved (
move(h.s)is an error), and only when it holds the one reference to a new value: nothing else refers to it or to anything it reaches. A parameter can be moved only when it is[Move], since the caller handed its value over.main.pep:8:24: error: 'a' cannot be moved: 'b' also references its value 8 | names.replace(move(a)); | ^ note: only a variable that holds the one reference to a new value can hand it over; pass 'copy(...)' instead -
A moved variable cannot be read again until it is assigned a new value, including on the next pass through a loop:
main.pep:8:12: error: 'a' is used after its value was moved 8 | return a.count; | ^ main.pep:7:19: note: 'a' was moved here; assign it a new value before using it again 7 | names.replace(move(a)); | ^~~~
The runtime also moves values out of raw memory (move(buffer[head])), which
is a promise the compiler cannot check: only runtime files may do it. The
parameter of move is marked [MoveSource], which is also only allowed in
the runtime, so a hand-off always shows at the call as move(x); a function
of your own that takes over its argument uses [Move].
Where objects live
Every object, String buffer and Array buffer comes from the runtime’s
allocator. A block of up to 128 bytes (most objects and short strings)
comes from a small block allocator: blocks are grouped by size into
classes (multiples of 16 bytes up to 128, then four classes per power of
two). Each class’s blocks are cut from 64 KiB spans, and a thread hands
out a class’s blocks from one span at a time, so objects made one after
another sit close together in memory. Allocating, and freeing on the
thread that allocated, take no lock; a block freed on another thread goes
back to its span with one atomic step. Larger blocks come from the C
library’s calloc and go back with free. A small block’s size is
rounded up to its class, so a 40-byte object takes 48 bytes.
An empty Array allocates nothing. Array<T>() makes no object until the
Array is written (append, insert, reserve, …) or shared (assigned to
a second variable, stored in a field or element, passed or returned):
reading it (count, is_empty, contains, a for loop over it)
allocates nothing. The first write or share allocates the object, so every
name for the Array still sees the same appends:
struct Node {
String name;
# Most nodes never get children, and an empty Array costs nothing.
Array<Node> children = Array<Node>();
construct(name: String) {
this.name = name;
}
}
main(): int {
var a = Array<int>(); # no allocation
assert(a.count == 0 && a.is_empty, "reading allocates nothing");
for (var x in a) {
assert(false, "an empty Array has no elements");
}
var b = a; # allocates the Array: a and b share it
b.append(1); # allocates its buffer
assert(a.count == 1, "both names see the append");
var leaf = Node("leaf");
var root = Node("root");
root.children.append(leaf);
assert(leaf.children.is_empty && root.children.count == 1, "only root's children were allocated");
return 0;
}
Structs that hold an Array which usually stays empty (a tree node’s children, a list of comments) cost nothing extra for it.
The environment variable PEPPER_SMALL_BLOCK_LIMIT, read when the program
starts, sets the largest size the small block allocator serves, in bytes,
up to 4096 (a larger value counts as 4096). 0 turns it off, and then
every block comes from the C library:
PEPPER_SMALL_BLOCK_LIMIT=0 ./main # every block from calloc
PEPPER_SMALL_BLOCK_LIMIT=512 ./main # blocks up to 512 bytes from the classes
With it off, a program can use another C allocator, such as mimalloc, by
loading it in front of the C library (LD_PRELOAD=libmimalloc.so on
Linux).
Small blocks live in address space the program reserves at start (4 GiB,
committed 64 KiB at a time as it is used, and never returned to the
operating system); a program that fills it falls back to calloc. On
WebAssembly, which cannot reserve address space, every block comes from
calloc.
Summary
| Code | What happens to references |
|---|---|
var b = a; | b takes a new reference to a’s object. |
b = c; | b takes a reference to c’s object and holds it, then releases its old one. A destructor that runs then and reads b finds c’s object. |
f(a) | The parameter takes a reference for the duration of the call. |
return a; | The caller receives a reference to a’s object. |
a.method() | No change: this refers to a’s object. |
f(move(a)) | No change: a’s reference goes to the call, and a is left null. |
make(); (result unused) | The result is released at the end of the statement. |
var w = Weak<T>(a); | No change to a’s count; w does not keep the object alive. |
var c = copy(a); | c refers to new objects; a’s count does not change. |
| end of a block | Every local declared in the block releases its reference. |
Generics
Generics let you write a struct or a function once and use it with many types.
Add type parameters in angle brackets after the name, then use them like any
other type name. The standard library’s Array<T>,
HashMap<K, V>, and
Result<TValue, TError> are all ordinary generic
types written this way (Result is a generic enum).
Pepper generics work like C++ templates: each distinct set of type arguments,
such as Stack<int> or Stack<String>, produces its own copy of the code,
compiled for those exact types. There is no runtime cost for using them.
Generic structs
# `T` is a type parameter: each use such as `Stack<int>` or
# `Stack<String>` makes a separate, fully typed version of the struct.
struct Stack<T> {
var items: Array<T>;
construct() {
items = Array<T>();
}
push(item: T) {
items.append(item);
}
pop(): T {
return items.pop();
}
peek(): T {
# `top` is inferred as `T`: an `int` in `Stack<int>`, a `String`
# in `Stack<String>`.
var top = items.last();
return top;
}
is_empty(): bool {
return items.is_empty;
}
}
# Several parameters are separated by commas.
struct Entry<K, V> {
var key: K;
var value: V;
construct(K key, V value) {
this.key = key;
this.value = value;
}
}
main(): int {
var numbers = Stack<int>();
numbers.push(1);
numbers.push(2);
assert(numbers.peek() == 2, "peek looks at the top");
assert(numbers.pop() == 2, "last in, first out");
assert(numbers.pop() == 1, "then the first");
assert(numbers.is_empty(), "empty again");
var words = Stack<String>();
words.push("hello");
assert(words.pop() == "hello", "the same code works for Strings");
var e = Entry<String, int>("answer", 42);
assert(e.key == "answer" && e.value == 42, "two type parameters");
return 0;
}
- Name the type parameters after the struct’s name:
struct Stack<T>,struct Entry<K, V>. Inside the struct, fields, parameters, return types, and locals can all use them. - Name the type arguments whenever you use the struct:
Stack<int>(),var s: Stack<String>;. Stack<int>andStack<String>are different types.- A type argument is a type. The one exception is a number, which only
the lane count of a SIMD vector accepts (
Vector<int, 4>); a struct of your own cannot compute with a number parameter. - Each type argument makes a separate copy of the struct, so a struct whose
copy needs a copy with a larger argument never ends:
struct Foo<T>with a fieldvar inner: Foo<Array<T>>makesFoo<int>needFoo<Array<int>>, which needsFoo<Array<Array<int>>>, and so on. The compiler stops 64 levels deep with an error at the type that asks for the next level ('Foo' is specialized 65 levels deep, past the limit of 64), with a note where the chain started.
Generic functions
A free function takes type parameters the same way:
larger<T>(a: T, b: T): T {
if (a > b) {
return a;
}
return b;
}
struct Circle {
var radius: int = 1;
area(): int { return 3 * radius * radius; }
}
struct Square {
var side: int = 2;
area(): int { return side * side; }
}
# The body can use anything the type argument provides: this works for
# any T that has an `area()` method returning a number.
total_area<T>(shapes: Array<T>): int {
var sum = 0;
for (var shape in shapes) {
sum += shape.area();
}
return sum;
}
# `T()` calls the type argument's constructor.
fresh<T>(): Array<T> {
var items = Array<T>();
items.append(T());
return items;
}
# A closure in generic code is compiled with each use too: for `T = int`
# it calls `larger<int>`.
larger_later<T>(a: T, b: T): () => T {
return () => larger(a, b);
}
main(): int {
# Type arguments come from the arguments, or can be written out.
assert(larger(3, 9) == 9, "ints");
assert(larger<float>(2.5, 1.5) == 2.5, "floats");
var circles = Array<Circle>();
circles.append(Circle());
circles.append(Circle());
assert(total_area(circles) == 6, "circles");
var squares = Array<Square>();
squares.append(Square());
assert(total_area<Square>(squares) == 4, "squares");
assert(total_area(fresh<Circle>()) == 3, "a constructed Circle");
var pick = larger_later(4, 7);
assert(pick() == 7, "a closure calling a generic function");
return 0;
}
Pepper infers the type arguments from the arguments, so larger(3, 9) calls
larger<int>. You can always write them out, larger<float>(2.5, 1.5), or
write only the leading ones and let the arguments give the rest:
pick<int>(1, "x") for pick<T, U>(a: T, b: U). You must write them when the
arguments do not determine every type parameter, for example one that appears
only in the return type:
main.pep:4:13: error: cannot infer the type arguments of 'make' from its arguments; write them: make<T>(...)
4 | var b = make();
| ^~~~
main.pep:1:1: note: declared here
1 | make<T>(): Box<T> { return Box<T>(); }
| ^~~~
A generic function used as a value, not called, takes its type arguments
from the function type it is given: var f: (int, int) => int = larger; is
larger<int> (see Function types).
A parameter whose type is a trait takes its type arguments from the trait the
argument’s type declares: given total<T>(items: Iterator<T>), the call
total(Countdown(4)) infers T = int from extend Countdown with Iterator<int>.
Inference works through closures in both directions. In
transform(words, (w) => w.size), where transform<T, U>(items: Array<T>, f: (T) => U), words gives T, which types the closure’s parameter w, and
the closure’s result gives U.
A closure inside a generic function or method is compiled with each use of
it, so the generic calls in its body take their type arguments from that
use: in larger_later<T>(a: T, b: T) above, () => larger(a, b) calls
larger<int> when T is int.
Because each use is compiled for its concrete types, the body may use any
operator, field, or method that the type arguments provide. total_area<T>
calls shape.area(), so it works with any T that has an area() method
returning a number, and using it with a type that lacks one is a compile
error inside total_area. To state the requirement up front, and have the
compiler report it where the function is used, give T a
constraint.
That includes constructing one: T() (or T(args)) calls the type
argument’s constructor, so fresh<Circle>() above makes a Circle. In a
constrained generic, which offers only what
its constraints declare, it is an error.
Generic methods
A method can have type parameters of its own, in a struct, a generic struct, an extension, or a trait’s default method. They are inferred at each call like a function’s, or written after the method name:
struct Box<T> {
T value;
construct(T v) { value = v; }
# A method with a type parameter of its own: `U` is chosen per call.
map<U>(f: (T) => U): Box<U> {
return Box<U>(f(value));
}
}
main(): int {
var b = Box<int>(4);
# `U` comes from what the closure returns...
var tens = b.map((x) => x * 10);
assert(tens.value == 40, "tens");
var text = tens.map((x) => "forty");
assert(text.value == "forty", "text");
# ...or is written after the method name.
var wide = b.map<long>((x: int): long => x + 1);
assert(wide.value == 5, "wide");
return 0;
}
- Write the type arguments with no space before
<or after>:b.map<long>(f). With a space,a.b < cis a comparison. - A generic method cannot be called through a trait value, since each of its uses is compiled separately. Call it on the concrete type.
- The iterator adapters
map,filter,enumerateandzipare generic default methods ofIterator<T>; see Iteration.
Default type arguments
A type parameter can have a default, written Name=Type. When a use leaves
that argument out, the default fills it in:
struct Measurement<TValue, TUnit=String> {
var value: TValue;
var unit: TUnit;
}
main(): int {
# `Measurement<float>` fills TUnit with its default, String.
var length = Measurement<float>();
length.value = 2.5;
length.unit = "m";
assert(length.unit == "m", "TUnit defaulted to String");
# Any argument can still be given explicitly.
var count = Measurement<int, int>();
count.unit = 7;
assert(count.unit == 7, "TUnit given as int");
# The standard library's Result uses a default the same way:
# Result<int> means Result<int, Error>.
var ok = Result<int>.ok(42);
assert(ok.value() == 42, "Result with the default error type");
return 0;
}
Arguments are matched to parameters in order, so put the parameters that have
defaults last. The standard library declares enum Result<TValue, TError=Error>, so
Result<int> means Result<int, Error>. See
Error handling with Result.
Constraints
A type parameter can require a trait:
write the trait after a colon, T: Shape. Every type argument for T must
then conform to Shape. Several constraints are joined with &, and a
constrained parameter can still have a default, written after them:
trait Shape {
area(): int;
}
trait Named {
name(): String;
}
struct Circle {
var radius: int = 1;
area(): int { return 3 * radius * radius; }
}
extend Circle with Shape;
struct Square {
var side: int = 2;
area(): int { return side * side; }
name(): String { return "square"; }
}
extend Square with Shape;
extend Square with Named;
# Every T must conform to Shape.
total_area<T: Shape>(shapes: Array<T>): int {
var sum = 0;
for (var shape in shapes) {
sum += shape.area();
}
return sum;
}
# Several constraints are joined with `&`.
label<T: Shape & Named>(item: T): String { return item.name(); }
# A constrained parameter can have a default, which must conform too.
struct Gallery<TTag, TShape: Shape = Square> {
var items = Array<TShape>();
add(item: TShape) { items.append(item); }
total(): int { return total_area(items); }
}
main(): int {
var circles = Array<Circle>();
circles.append(Circle());
circles.append(Circle());
assert(total_area(circles) == 6, "circles");
assert(label(Square()) == "square", "label");
# Gallery<String> is Gallery<String, Square>.
var gallery = Gallery<String>();
gallery.add(Square());
assert(gallery.total() == 4, "default");
# A trait type conforms to its own trait.
var shapes = Array<Shape>();
shapes.append(Circle());
shapes.append(Square());
assert(total_area(shapes) == 7, "trait values");
return 0;
}
Constraints work on generic structs, enums whose cases carry values,
functions, methods, traits and type aliases. A constraint may name the
parameters before it in the same list: sum<T, I: Iterator<T>>(values: I).
A type argument conforms to a trait when:
- it is a struct declared to have the trait with
extend Circle with Shape. For a generic struct,extend Box<T> with Shapemakes everyBox<X>conform, andextend Box<int> with ShapeonlyBox<int>. A generic trait must match exactly:extend Counter with Iterator<int>conforms toIterator<int>, not toIterator<long>. Having the right methods is not enough; the extension is what declares it. - it is the trait type itself (
Array<Shape>passed asArray<T>withT: Shape), as long as every member of the trait can be called through a value of the trait type (see traits as types). - it is a type parameter constrained by that trait: inside
wrap<T: Shape>,Gallery<String, T>is allowed.
A number type, char, bool or an enum whose cases carry no values
conforms when it declares the trait the same way (extend int with Shape;
see numbers, bool and enums),
for constraints only: it never becomes a trait value. Pointers, tuples and
function types conform to no trait.
The Shared constraint
T: Shared is built in: it requires a type that is safe to share between
threads, by the same rule as a field of a [Shared] struct. The types that
conform are listed in
What threads can share; no extend
can make another type conform. It gives T no methods, and it joins other
constraints with & (T: Shared & Named):
import async;
[Shared]
struct Settings {
const workers: int;
construct(this.workers = 1) {}
}
# A cell that any thread may read: its value must be thread-safe.
[Shared]
struct Cell<T: Shared> {
const value: T;
construct(value: T) { this.value = value; }
}
share<T: Shared>(value: T): Cell<T> { return Cell<T>(value); }
main(): int {
var count = share(3);
var settings = share(Settings(4));
assert(count.value == 3 and settings.value.workers == 4, "shared cells");
# The runtime states the same rule: spawn<T: Shared>.
async.run(() => {
var task = async.spawn<Settings>(() => { return Settings(2); });
assert(task.join().value().workers == 2, "spawned");
});
return 0;
}
The standard library states the rule this way where it applies:
spawn<T: Shared>, run_blocking<T: Shared> and Lazy<T: Shared>, so
async.spawn<String>(...) is reported at String, with a note saying why
it is not thread-safe ('String' is not declared [Shared]). In a
constraint, Shared always means this built-in, so a trait cannot be
named Shared. A generic that passes its own T to one of these (say
run_blocking<T>) needs T: Shared too.
A constraint can name another type parameter of the same declaration:
count_all<T, I: Iterator<T>>(source: I). Inference reads it too: the
argument gives I, and the Iterator<...> that I declares gives T. A
trait’s default method may name the trait’s own parameters
(put_all<I: Iterator<T>> in trait Sink<T>), which take the arguments of
the conformance (extend Sum with Sink<int>).
# `I`'s constraint names `T`: the argument's type gives `I`, and the
# `Iterator<T>` it declares gives `T`.
count_all<T, I: Iterator<T>>(source: I): int {
var count = 0;
while (source.next()) {
count += 1;
}
return count;
}
first<T, I: Iterator<T>>(source: I): T {
source.next();
return source.current();
}
main(): int {
var words = Array<String>();
words.append("pepper");
words.append("salt");
# Both inferred: I = ArrayIterator<String>, T = String.
assert(count_all(words.iterate()) == 2, "count");
assert(first(words.iterate()) == "pepper", "first");
# Written type arguments may stop early; the rest are inferred.
assert(count_all<String>(words.iterate()) == 2, "partial");
return 0;
}
The compiler checks every use: a type argument you write, one it infers from a call, a default, and a concrete override. The error points at the type argument, or at the call argument the type came from:
main.pep:11:21: error: 'Circle' does not conform to trait 'Shape', required by 'T' in 'Gallery'
11 | var g = Gallery<Circle>();
| ^~~~~~
main.pep:7:19: note: constraint declared here
7 | struct Gallery<T: Shape> {
| ^~~~~
main.pep:4:8: note: declare it with 'extend Circle with Shape'
4 | struct Circle {
| ^~~~~~
A type parameter passed on to a constrained one must carry the constraint
itself: wrap<T>(item: T): Gallery<String, T> is an error that suggests
T: Shape. The language server offers both the extension and the constraint
as quick fixes.
- A constraint must name a trait:
T: Point, withPointa struct, is an error. - An extension of a constrained struct keeps its constraints:
extend Gallery<TTag, TShape>needs noTShape: Shapeof its own. You may restate them as documentation,extend Gallery<TTag, TShape: Shape>. A constraint the struct does not have makes a constrained extension. - In an overload set, a generic member whose inferred type arguments do not meet its constraints is not viable, and the error lists why.
- A constrained name is always a type parameter, never an override’s type
argument:
describe<Point: Shape>declares a generic function whose parameter happens to be namedPoint.
Inside a constrained generic
The compiler also checks the generic itself, once, whether or not anything
uses it: inside largest<T: Shape & Ordered>, a value of type T offers
exactly what Shape and Ordered declare. It may:
- call their instance methods and operators (
item.area(),best < item), - call their static members through the parameter (
T.unit()), - convert to one of their trait types (
var shape: Shape = item;), - be stored, passed, returned, and compared with
==,!=,===and!==.
trait Shape {
area(): int;
scaled(by: int): this;
static unit(): this;
}
trait Ordered {
<(other: this): bool;
}
struct Square {
var side: int = 1;
construct(this.side = 1) {}
area(): int { return side * side; }
scaled(by: int): Square { return Square(side * by); }
static unit(): Square { return Square(1); }
<(other: Square): bool { return side < other.side; }
}
extend Square with Shape;
extend Square with Ordered;
# Inside the body, a T offers what Shape and Ordered declare.
largest<T: Shape & Ordered>(items: Array<T>): T {
var best = items[0];
for (var item in items) {
if (best < item) {
best = item;
}
}
return best;
}
# Static members are called through the parameter.
unit_area<T: Shape>(): int { return T.unit().area(); }
# A T converts to the trait types of its constraints.
as_shape<T: Shape>(item: T): Shape {
var shape: Shape = item.scaled(2);
return shape;
}
main(): int {
var squares = Array<Square>();
squares.append(Square(2));
squares.append(Square(3));
assert(largest(squares).side == 3, "largest");
assert(unit_area<Square>() == 1, "static member");
assert(as_shape(Square(3)).area() == 36, "conversion");
return 0;
}
Anything else is an error at the member or operator, even before any use:
a field, a method no constraint declares, a constructor T(), a call of a
constraint’s method with the wrong number or types of arguments, or
initializing, assigning or returning a T as another type. (Passing a T
to a function that takes another type is still reported in each
specialization.)
main.pep:5:45: error: no member named 'perimeter' in 'T'
5 | total<T: Shape>(item: T): int { return item.perimeter(); }
| ^~~~~~~~~
main.pep:5:10: note: 'T' is constrained by 'Shape', which has no member 'perimeter'
5 | total<T: Shape>(item: T): int { return item.perimeter(); }
| ^~~~~
A method that two constraints both declare is an error too
('name' is declared by both 'Named' and 'Titled'; call it through one of them).
Each use is still compiled for its type argument, so a call goes straight
to that type’s own method, with no trait value in between. A parameter
without a constraint keeps working as before: its uses are checked in each
specialization. In a list that mixes the two, such as f<T: Shape, U>, only
the uses of T are checked in the generic.
Constrained extensions
An extension of a generic struct can add a constraint the struct does not
have: extend Box<T: Named> { ... }. Its members exist only on the
specializations whose type argument meets it, so Box<Square> has
label() and Box<String> does not. With with, the constraint makes a
conditional conformance: after extend Box<T: Shape> with Shape, a
Box<Square> is a Shape (it can be passed where T: Shape is required
and converted to a Shape value), and a Box<String> is not.
trait Shape {
area(): int;
}
trait Named {
name(): String;
}
struct Square {
area(): int { return 4; }
name(): String { return "square"; }
}
extend Square with Shape;
extend Square with Named;
struct Box<T> {
var item: T;
construct(item: T) { this.item = item; }
}
# Only a Box of something Named has `label()`.
extend Box<T: Named> {
label(): String { return "box of " + item.name(); }
}
# A Box of shapes is itself a shape.
extend Box<T: Shape> with Shape {
area(): int { return item.area() * 2; }
}
main(): int {
var box = Box<Square>(Square());
assert(box.label() == "box of square", "constrained extension");
Shape shape = box;
assert(shape.area() == 8, "conditional conformance");
# Box<String> is still a Box, without `label()` or `area()`.
var text = Box<String>("plain");
assert(text.item == "plain", "any Box");
return 0;
}
- Every
Box<X>keeps the struct’s own members and constraints; an extension adds to them, it never removes any. - Inside the extension,
Toffers the struct’s constraints and the extension’s, as in any constrained generic. - A call of a member the specialization does not have names the constraint it misses:
main.pep:20:9: error: no member named 'label' in 'Box<Circle>'
20 | box.label();
| ^~~~~
main.pep:15:15: note: 'label' requires 'Circle' to conform to 'Named'
15 | extend Box<T: Named> {
| ^~~~~
and a type argument that misses a conditional conformance says so too
('Box<String>' conforms to 'Shape' only when 'String' conforms to 'Shape').
- Only the struct’s own parameters can carry an extension’s constraints:
extend Box<int: Shape>is an error. - A conformance that holds for every
Boxneeds its members on everyBox:extend Box<T> with Namedcannot rest on aname()that onlyextend Box<T: Named>provides. Make the conformance conditional instead (extend Box<T: Named> with Named). - Several extensions may declare the same trait with different constraints; a specialization conforms when it meets any of them.
- Inside another generic, a call such as
b.label()on aBox<T>is checked in each specialization, like any member of a generic struct.
Overriding a generic function for one type
You can give a generic function a separate definition for a specific type argument. Write the concrete type where the type parameter would go:
# The generic version handles every type...
describe<T>(value: T): String {
return "something";
}
# ...and these definitions replace it for two specific types.
describe<int>(value: int): String {
return "an int";
}
describe<String>(value: String): String {
return "a String";
}
main(): int {
assert(describe<float>(1.5) == "something", "the generic version");
assert(describe<int>(7) == "an int", "the int override");
assert(describe<String>("hi") == "a String", "the String override");
# The standard library's hash<String> is such an override: it hashes
# a String by its content instead of by identity.
assert(hash<String>("pepper") == hash<String>("pepper"), "content hash");
return 0;
}
A call whose type argument matches an override, such as describe<int>(7),
uses the override; every other call uses the generic version. The standard
library uses this to give hash<String> a
content-based hash, while hash<T> hashes other structs by identity; a
struct keyed by its content overrides hash<T> the same way (see
Hashing and equality).
An override can name a built-in type (int, bool, String, and so on) or
a struct, such as describe<Point>. It must be declared next to the generic
function or next to one of its type arguments: in the file that declares
either, or in another file of the same module. Here
main.pep overrides shapes.pep’s describe<T> for its own Point, and
the generic code in shapes.pep uses the override too:
# A generic function, and code that calls it for any type.
export describe<T>(v: T): int {
return 0;
}
export describe_twice<T>(v: T): int {
return describe<T>(v) * 2;
}
import "shapes";
struct Point {
var x: int;
var y: int;
}
# Overrides shapes.pep's describe<T> for Point, from another file.
describe<Point>(p: Point): int {
return p.x + p.y;
}
main(): int {
var p = Point();
p.x = 3;
p.y = 4;
assert(describe<Point>(p) == 7, "the override");
assert(describe_twice<Point>(p) == 14, "generic code in shapes.pep uses it too");
assert(describe<int>(5) == 0, "other types keep the generic version");
# The standard library's generics, such as hash<T>, can be overridden
# for a struct the same way.
return 0;
}
A function whose type parameters all name types the file can see (its own,
the ones it imports, under any name such as P after
import [Point as P] from geo, and the standard library’s) is an override
when the file can also see a generic function of that name and number of
type parameters (its own, an imported one, or the standard library’s);
otherwise a struct’s name is just the name of a type parameter. The generic
may be spelled by any name the file has for it (show<Point> after
import [describe as show]). A type argument must name a struct, not a type
alias or a trait. A generic and another function of the same name in one
file are overloads, unless they have the
same parameters, or the generic’s type parameters all name structs the file
can see without a generic of that name to override: either is an error,
since it usually means an override whose type argument the file cannot see,
or of a generic the file’s own function of that name hides.
This placement rule (Rust calls it the orphan rule) means a file that can see both the generic and the type can also see every override that applies to them, so the compiler can check each file on its own. An override that neither of them is declared beside is an error:
main.pep:2:1: error: cannot override 'hash' for 'int' here: an override must be in the file or module that declares 'hash' or one of its type arguments
2 | hash<int>(value: int): ulong {
| ^~~~
The standard library’s generics, such as hash<T> and to_string<T>,
can be overridden for your own structs, but not for built-in types: the
standard library declares both. Wrap the built-in type in a struct of your
own instead.
To add methods to a generic struct from outside its declaration, use an extension.
Reflection and attributes
A program can look at its own types while it compiles: list a struct’s fields, read the attributes written on them, and generate code for each one. This is how one function can turn any struct into JSON, declare a command-line option per field, or compare two values field by field.
The work happens in the compiler, in a static block, so it costs nothing
at run time and needs no type information in the program. This is the model
of Zig’s comptime and C++’s static reflection, not of Java or C# runtime
reflection.
struct Point {
var x: int;
var y: int;
var label: String;
}
# One function describes any struct: the `static` block runs in the
# compiler, once per type `describe` is called with.
describe<T>(value: T): String {
var out = StringBuilder();
static (out, value) {
out.append(reflect.name<T>());
out.append("(");
for (var field in reflect.properties<T>()) {
# `field.name` is a compile-time string; `field.get(value)` reads
# the field. Both statements use `out`, a captured runtime
# value, so they are emitted once per field.
out.append($"{field.name} = {field.get(value)}");
if (!field.is_last) {
out.append(", ");
}
}
}
out.append(")");
return out.to_string();
}
main(): int {
var p = Point();
p.x = 3;
p.y = 4;
p.label = "corner";
assert(describe(p) == "Point(x = 3, y = 4, label = corner)", "one line per field, in declaration order");
return 0;
}
static blocks
A static block is written inside a function. It names, in parentheses, the
runtime values it may emit code against: its capture list.
static (out, value) {
...
}
static { ... } with no list is pure compile-time work, such as a check
with reflect.assert.
Each statement in the block either runs in the compiler or is emitted into the function:
- A statement that uses a captured name, or a runtime local the block
declares, is emitted. Its compile-time parts are filled in first:
out.append(field.name)emitsout.append("x"). - Everything else runs in the compiler:
vardeclarations whose value uses no captured name,ifon a compile-time condition (only the branch taken is kept),forover a compile-time list, which is unrolled, one copy of its body per element, andwhileon a compile-time condition, which emits its body once per iteration. - An
ifwhose condition uses a captured name is emitted as a runtimeif; its branches can still hold compile-time steps.
A var with a captured name in its value is a runtime local, and using it
counts as using a capture. Using a runtime value that is not captured is an
error at that name:
main.pep:9:24: error: 'total' is a runtime value; add it to the static block's capture list
Each pass of an unrolled for or while, and the branch an if keeps, is
its own block, like the body of a real loop.
A closure in an emitted statement is an ordinary closure: it captures
runtime locals as any closure does. Its body cannot use a compile-time value
such as field.name, since the closure outlives the block’s expansion; copy
the value into a runtime local first, and the closure captures that local:
static (options, calls) {
for (var field in reflect.fields<Options>()) {
var text = $"{field.get(options)}";
calls.append(() => text);
}
}
Compile-time loops
break and continue in a compile-time for or while run in the
compiler: continue drops the rest of the pass, and break ends the
unrolling. They cannot sit in a runtime if, whose condition the compiler
does not know:
main.pep:10:17: error: 'break' in a runtime 'if' cannot leave a compile-time loop; the condition is not known while compiling
A while that runs 100,000 times is taken to be endless and stops the
compile at its while.
struct Row {
var id: int;
var _cache: int;
var name: String;
var score: int;
var notes: String;
}
# The sum of the public `int` fields of `Row` before 'notes': `continue`
# and `break` steer the unrolled `for`.
sum_leading_ints(row: Row): int {
var total = 0;
static (total, row) {
for (var field in reflect.fields<Row>()) {
if (field.name.starts_with("_")) {
continue;
}
if (field.name == "notes") {
break;
}
if (reflect.is_same<field.Type, int>()) {
total += field.get(row);
}
}
}
return total;
}
# A compile-time `while` emits its body once per iteration: here, the
# first four powers of three, as four additions.
powers_of_three(): int {
var sum = 0;
static (sum) {
var power = 1;
while (power < 30) {
sum += power;
power = power * 3;
}
}
return sum;
}
main(): int {
var row = Row();
row.id = 2;
row.score = 40;
assert(sum_leading_ints(row) == 42, "id and score; _cache is skipped");
assert(powers_of_three() == 1 + 3 + 9 + 27, "one addition per iteration");
return 0;
}
In generic code
In a generic function, a static block runs once for each specialization,
when its type arguments are known: describe<Point> and describe<Line>
each get their own expansion. Code the block emits can call other generic
functions, such as describe(field.get(value)) for a field that is itself a
struct, and those are specialized in turn. A mistake in a block is reported
when some specialization uses it, with the pass it happened in and, when a
generic asked for that specialization, a note at each use that led to it
('dump<Point>' is used by 'main' here):
main.pep:14:13: error: 'print' cannot be called at compile time
main.pep:12:27: note: in pass 1 of 2 (member 'x')
note: a 'static' block can call the 'reflect' functions and the methods of what they return; calling other functions at compile time is not supported yet
main.pep:10:1: note: in the static block of 'dump<Point>'
At module scope
A static block outside any function runs once, while compiling. It has
nothing to capture and emits no code; instead, each const declared at the
top of the block becomes a module-scope constant holding what the block
computed. Its other vars, and anything declared in its loops and
branches, stay in the compiler. export static exports its constants, and
attributes written above the block ([Target(...)]) apply to them too.
export struct Pixel {
var red: u8;
var green: u8;
var blue: u8;
var alpha: u8;
}
export enum Channel {
Red,
Green,
Blue,
Alpha
}
# A module-scope `static` block runs once, while compiling. Each `const` at
# its top becomes a module-scope constant holding what the block computed;
# `export static` exports them.
export static {
var bytes = 0;
for (var field in reflect.properties<Pixel>()) {
reflect.assert(reflect.is_same<field.Type, u8>(), $"'{field.name}' must be one byte");
bytes += 1;
}
const PIXEL_BYTES = bytes;
const LAST_CHANNEL = reflect.cases<Channel>()[reflect.cases<Channel>().count - 1].get();
}
import "sizes";
main(): int {
assert(PIXEL_BYTES == 4, "one byte per field of Pixel");
assert(LAST_CHANNEL == Channel.Alpha, "the last case of Channel");
return 0;
}
A constant takes a number, bool, character or enum case; a
module-scope variable cannot be a String, since it must
be thread-safe. At module scope, a case’s get() is the case itself, and a
statement that would emit code is an error:
main.pep:7:9: error: a module-scope 'static' block cannot emit code; declare a 'const' with what it computes
What the compiler can evaluate
Compile-time values are integers, floating-point numbers, bool, strings,
enum cases, and what the reflect functions return: members, cases,
functions, lists and attributes. Expressions can use literals, the
arithmetic, comparison and logical operators, + on strings, interpolated
strings ($"{field.name}: "), ?:, assignment to a compile-time var
(n += 1), and indexing a list. A string has length, is_empty,
starts_with, ends_with and contains; a list has count and
is_empty.
Emitted code can use a compile-time number, bool, string or enum case;
the compiler writes it as a literal.
Seeing what a block does
A static block runs inside the compiler, so a print in it would be
emitted into the program instead of showing anything while compiling.
These show what the block computes:
-
reflect.print(value)writesvalueas a note each time the compiler runs it, on every compile, including one that succeeds. In an unrolled loop each note says which pass printed it:main.pep:9:17: note: static print: "describing Point" 9 | reflect.print($"describing {reflect.name<T>()}"); | ^~~~~ main.pep:11:21: note: static print: "x" 11 | reflect.print(field.name); | ^~~~~ main.pep:10:27: note: in pass 1 of 2 (member 'x') 10 | for (var field in reflect.properties<T>()) { | ^~~~~~~~~~~~~~~~~~~~~~~ -
reflect.assert(condition, message)andreflect.error(message)stop the compile at that point withmessage, which can be an interpolated string holding the values that matter (reflect.assert(n < 8, $"{reflect.name<T>()} has {n} fields")). Like every error in a block, it carries the pass and, in generic code, the specialization it happened in (see In generic code). -
The emitted code runs like code written by hand. Each statement a block emits keeps the source line of the statement it came from, so a runtime
printit emits shows values as the program runs, and a debugger breakpoint on that line (with-g) stops there, in every pass. Stepping goes through an unrolled loop pass by pass, stopping on theforline and then on the body, and the debugger shows the compile-time variables, such as the loop’sfield(see Stepping through astaticblock). -
--emit-staticwrites the program’s source with each block replaced by what it emits (see the next section). -
--emit-llvmwrites the program’s LLVM IR into the build directory (.pep/build/<name>.ll), where the function a block expanded in shows exactly what it emitted, one copy per specialization (describe<Point>).--emit-astprints the program as parsed, before any block runs, so it does not show the expansion.
Seeing what a static block emits
pepper build main.pep --emit-static writes a copy of the program with
each static block replaced by the statements it emits, the way a C
compiler’s -E shows the preprocessed source. Nothing is compiled; the
copy goes into main.static/ (or the folder -o names) and builds into
the same program. For the describe example at the top of this page, the
copy keeps describe<T> as written and adds what it is for Point:
# 'describe' specialized for T = Point, with its static blocks expanded
describe<Point>(value: Point): String {
var out = StringBuilder();
# static (out, value) expanded from reflection-basics.pep:11:5 for T = Point
{
out.append("Point");
out.append("(");
{
out.append($"x = {value.x}");
{
out.append(", ");
}
}
{
out.append($"y = {value.y}");
{
out.append(", ");
}
}
{
out.append($"label = {value.label}");
}
}
# end static
out.append(")");
return out.to_string();
}
- Each pass of an unrolled
fororwhileis a block of its own, one after another; a compile-timeifleaves only the branch it took; the compile-timevars and steps are gone. - Compile-time values are written as literals:
field.namebecomes"x"(inside an interpolated string, its text),field.Typethe type,field.get(value)value.x,case.matches(v)v is Shape.Circle, andfunction.call()a call by name. A case’s value,item.get(v), becomes amatchthat reads it. - Everything else in the file is copied byte for byte, comments included.
--emit-static=keepalso keeps each block as#comments above its expansion. - A block in a generic function expands differently for each type, so the copy adds a concrete override per specialization the program uses, as above. A block in a method of a generic struct, or in a generic method, stays as written, with each specialization’s expansion above it as comments.
- An error in the emitted code shows when the copy is built, at the line of the copy that has it.
- To debug the program in the expanded source, build it with
-g --debug-static=expanded: the copy is written beside the program asmain.static.pep, and the debugger steps through it (see Stepping through astaticblock). In a method of a generic struct or a generic method, where the copy shows the expansion only as comments, the debugger shows the block as written.
pep-build lists the details.
The reflect functions
reflect is built in: a static block uses it without an import. The
reference has an entry for each function and
for the members, cases and functions they return.
| Function | Returns |
|---|---|
reflect.name<T>() | the type’s name, as written ("Box<int>") |
reflect.properties<T>() | the public fields and properties of a struct, in declaration order |
reflect.fields<T>() | every field of a struct, private ones and a property’s storage included; only in the file that declares T |
reflect.cases<E>() | an enum’s cases, in order |
reflect.functions_with<A>() | every function in the program that carries attribute A |
reflect.is_struct<T>(), is_enum<T>(), is_trait<T>() | what kind of type T is |
reflect.is_reference<T>() | whether T is a reference type, whose slots can be null: a struct or an enum whose cases carry values, unless it is [Value] |
reflect.is_same<A, B>() | whether A and B are the same type (int and i32 are) |
reflect.has_attribute<T, A>(), reflect.attribute<T, A>() | attribute A on struct or enum T |
reflect.has_build_option("NAME") | whether the build defines build option NAME (-D NAME) |
reflect.build_option("NAME"), reflect.build_option("NAME", default) | the build option’s value as a String; without a default, an error when the build does not define it |
reflect.print(value) | writes value as a note while compiling (see Seeing what a block does) |
reflect.error(message) | fails the compile with message |
reflect.assert(condition, message) | fails the compile with message when condition is false |
fields exists for code a struct’s author writes, such as a field-by-field
==; anywhere else it is an error, which points at properties:
main.pep:4:24: error: 'fields' of 'Account' is only available in the file that declares 'Account'; use 'properties'
A member (from properties or fields) has:
name,index,is_last,is_const,is_propertytype_name: its type as a string;field.Typeis the type itself, usable wherever a type is written:var copy: field.Type = ...,reflect.is_same<field.Type, String>(),parse<field.Type>(text). This works for a compile-timevarholding a member too (var first = case.values[0];givesfirst.Type).has_attribute<A>()andattribute<A>()get(value)andset(value, x), which emitvalue.nameandvalue.name = x
A case (from cases) has:
name,value(its integer value, or the tag of a case that can carry values),index,is_last,has_attribute<A>(),attribute<A>()values: the values the case carries, as members, in order; empty for a case that carries none. A value’sget(v)reads it fromv, which must hold this case;setis an error, since a case’s values are fixed.matches(v), which emits a test of whethervholds this caseget(values...), which emits the case made with those values:case.get()for a case that carries none,case.get(r)forCircle(radius: int)
A function (from functions_with) has name, has_attribute<A>(),
attribute<A>(), and call(arguments...), which emits a direct call.
enum Level {
Low,
High = 10,
}
struct Settings {
var name: String;
var retries: int;
var verbose: bool;
var level: Level;
}
# A compile-time `if` keeps only the branch it takes, so each field is
# written by the code that fits its type.
render<T>(value: T): String {
var out = StringBuilder();
static (out, value) {
for (var field in reflect.properties<T>()) {
out.append($"{field.name}=");
if (reflect.is_same<field.Type, String>()) {
out.append($"\"{field.get(value)}\"");
} else if (reflect.is_enum<field.Type>()) {
out.append(level_name(field.get(value)));
} else {
out.append($"{field.get(value)}");
}
out.append(";");
}
}
return out.to_string();
}
# `reflect.cases<E>()` lists an enum's cases; `case.get()` is the case
# itself in emitted code.
level_name(level: Level): String {
var name = "";
static (level, name) {
for (var case in reflect.cases<Level>()) {
if (level == case.get()) {
name = case.name;
}
}
}
return name;
}
# `field.set(value, x)` assigns a field.
reset_counts<T>(value: T) {
static (value) {
for (var field in reflect.properties<T>()) {
if (reflect.is_same<field.Type, int>()) {
field.set(value, 0);
}
}
}
}
main(): int {
var settings = Settings();
settings.name = "fast";
settings.retries = 3;
settings.level = Level.High;
assert(
render(settings) == "name=\"fast\";retries=3;verbose=false;level=High;",
"each field is written by the code for its type"
);
reset_counts(settings);
assert(settings.retries == 0, "set assigns the int fields");
return 0;
}
struct Account {
var owner: String;
var _balance: int;
construct(this.owner = "", this._balance = 0) {}
# `reflect.fields<T>()` lists private fields too, so it is available
# only in the file that declares `T`: code its author writes, like this
# field-by-field `==`.
==(other: Account): bool {
var same = true;
static (same, other) {
for (var field in reflect.fields<Account>()) {
same = same && field.get(this) == field.get(other);
}
}
return same;
}
}
main(): int {
assert(Account("ann", 5) == Account("ann", 5), "equal fields compare equal");
assert(!(Account("ann", 5) == Account("ann", 6)), "a private field takes part");
return 0;
}
enum Shape {
Circle(radius: int),
Rect(width: int, height: int),
Point,
}
# `case.matches(value)` tests which case a value holds, and each of the
# case's `values` reads its value with `get`.
describe<E>(value: E): String {
var out = "";
static (out, value) {
for (var case in reflect.cases<E>()) {
if (case.matches(value)) {
out = case.name;
for (var item in case.values) {
out = out + $" {item.name}={item.get(value)}";
}
}
}
}
return out;
}
# `case.get(values...)` makes a case; here, each value is `size`.
square<E>(name: String, size: int): E {
var out: E;
static (out, name, size) {
for (var case in reflect.cases<E>()) {
if (name == case.name) {
if (case.values.count == 0) {
out = case.get();
} else if (case.values.count == 1) {
out = case.get(size);
} else if (case.values.count == 2) {
out = case.get(size, size);
}
}
}
}
return out;
}
main(): int {
assert(describe(Shape.Rect(3, 4)) == "Rect width=3 height=4", "a case's values");
assert(describe(Shape.Point) == "Point", "a case without values");
assert(describe(square<Shape>("Rect", 5)) == "Rect width=5 height=5", "a made case");
assert(describe(square<Shape>("Circle", 2)) == "Circle radius=2", "one value");
return 0;
}
Attributes
An attribute is a struct marked [Attribute]. Writing it above a
declaration records a compile-time value of that struct, which
has_attribute<A>() tests for and attribute<A>() returns.
# An attribute is a struct marked `[Attribute]`. Its fields hold what a
# use of it says; a field with an initializer may be left out.
[Attribute]
struct Name {
const value: String;
}
[Attribute]
struct Ignore {}
struct User {
[Name("user_id")]
var id: int;
var email: String;
[Ignore]
var session: int;
}
# The keys a struct's fields are written under: each field's `[Name]` if
# it has one, else the field's own name, skipping `[Ignore]`d fields.
keys<T>(): String[] {
var keys: String[] = [];
static (keys) {
for (var field in reflect.properties<T>()) {
if (!field.has_attribute<Ignore>()) {
var key = field.name;
if (field.has_attribute<Name>()) {
key = field.attribute<Name>().value;
}
keys.append(key);
}
}
}
return keys;
}
main(): int {
var found = keys<User>();
assert(found.count == 2, "the [Ignore]d field is skipped");
assert(found[0] == "user_id", "[Name] renames the key");
assert(found[1] == "email", "a field without [Name] keeps its name");
return 0;
}
- Fields of an attribute struct are numbers,
bool,char,String, or enums whose cases carry no values. - Arguments are constants: literals, a negated number, an enum case
(
Level.High), or strings joined with+. They can be named:[Check(weight = 3)]. - Without a constructor, the arguments set the fields in order, and a field
with an initializer may be left out. A constructor may take
this.fieldparameters, or parameters its body assigns to fields (this.long_name = long_name;), which is all its body may do. - An attribute from another module can be named through it, with or
without arguments:
[serialize.Ignore],[serialize.Name("key")]. - An attribute that names neither a built-in attribute nor an
[Attribute]struct is an error, at its name:unknown attribute 'Optoin'. - Built-in attributes (
Target,Shared,Deprecated, …) are[Attribute]structs too, declared in the runtime (attributes), so a block reads them like its own:function.attribute<Deprecated>().message.Targetholds its condition as written ("!OS(Windows)"). The compiler still checks and acts on each one itself.
struct Settings {
var width: int;
[Deprecated("use 'width' instead", "width")]
var size: int;
}
# Lists the deprecated fields of `T` with what replaces each, read from the
# built-in `Deprecated` attribute like any `[Attribute]` struct.
deprecated_fields<T>(): String {
var out = "";
static (out) {
for (var field in reflect.properties<T>()) {
if (field.has_attribute<Deprecated>()) {
out = out + $"{field.name} -> {field.attribute<Deprecated>().replacement};";
}
}
}
return out;
}
main(): int {
assert(deprecated_fields<Settings>() == "size -> width;", "one deprecated field");
return 0;
}
Attributes can go on structs, enums and their cases, fields, functions, methods, constructors, parameters and module-scope variables.
[Attribute]
struct Check {
const weight: int = 1;
}
[Check]
check_addition(): bool {
return 1 + 1 == 2;
}
[Check(weight = 3)]
check_strings(): bool {
return "ab" + "c" == "abc";
}
# `reflect.functions_with<A>()` lists every function in the program that
# carries `A`; `check.call()` calls it directly.
run_checks(): int {
var score = 0;
static (score) {
for (var check in reflect.functions_with<Check>()) {
if (check.call()) {
score += check.attribute<Check>().weight;
}
}
}
return score;
}
main(): int {
assert(run_checks() == 4, "each [Check] function runs once, weighted");
return 0;
}
Not supported yet
- Calling functions other than the
reflectfunctions at compile time. - Reflecting methods and constructors (
reflect.methods<T>()). - Limiting where an attribute may be used (
[AttributeTargets(...)]). - Tracing the blocks the compiler runs.
- Type information at run time.
Type aliases
A type alias gives an existing type another name. Use one to shorten a long
type that you write often, such as HashMap<String, Array<int>>, or to say
what a value means, such as Score instead of int.
Declaring an alias
Write type, the new name, =, and the type it stands for:
# An alias is another name for an existing type.
type Score = int;
type Names = Array<String>;
type Handler = (int) => int;
struct Player {
var name: String;
var best: Score = 0; # aliases work in fields...
}
type Team = Player;
double_it(x: int): int {
return x * 2;
}
# ...and in parameters and return types.
apply(h: Handler, x: Score): Score {
return h(x);
}
main(): int {
var s: Score = 21;
var plain: int = s; # Score and int are the same type
assert(apply(double_it, plain) == 42, "function type alias");
var names = Names(); # construct through the alias
names.append("Ada");
assert(names.count == 1, "Names is Array<String>");
var p = Team();
p.best = s;
var q: Player = p; # Team and Player are the same type
assert(q.best == 21, "struct alias");
return 0;
}
An alias is only a spelling, not a new type. The compiler replaces every use
of the alias with its target before it checks anything else, so Score and
int are interchangeable: you can assign one to the other, pass one where the
other is expected, and error messages name the target type.
You can use an alias anywhere a type can appear: local and module-level
variables, parameters, return types, struct fields, generic arguments, and
casts. An alias of a struct also works where the struct’s name would: to
construct it (Team()) and to call its static methods.
Generic aliases
An alias can have type parameters of its own, including defaults, just like a generic struct:
# Generic aliases take their own type parameters.
type Predicate<T> = (T) => bool;
type Lookup<V> = HashMap<String, V>;
type Outcome<T, E=Error> = Result<T, E>;
is_even(x: int): bool {
return x % 2 == 0;
}
count_matching(values: Array<int>, test: Predicate<int>): int {
var n = 0;
for (var v in values) {
if (test(v)) {
n += 1;
}
}
return n;
}
parse_digit(text: String): Outcome<int> {
if (text == "7") {
return Outcome<int>.ok(7);
}
return Outcome<int>.err(Error("not a digit I know"));
}
main(): int {
var values = Array<int>();
values.append(1);
values.append(2);
values.append(4);
assert(count_matching(values, is_even) == 2, "Predicate<int> is (int) => bool");
var ages = Lookup<int>(); # HashMap<String, int>
ages.insert("Ada", 36);
assert(ages.contains("Ada"), "Lookup<int> is HashMap<String, int>");
# Outcome<int> fills E with its default: Result<int, Error>.
assert(parse_digit("7").value() == 7, "ok");
assert(parse_digit("x").is_err(), "err");
return 0;
}
The arguments at the use site fill the alias’s parameters in order, and
omitted ones take their defaults. Outcome<int> becomes
Result<int, Error>, and Lookup<int> becomes HashMap<String, int>.
Aliases are especially handy for function types, which
are long to write out: Predicate<int> reads better than (int) => bool.
Aliases across files
Like other top-level declarations, an alias is private to the file (or
module) that declares it. Mark it export to let files that import it use it; see
Visibility and Modules and imports.
Recursive aliases
An alias must not refer to itself, directly or through other aliases. The compiler reports the cycle at the alias, with a note at each alias it passes through:
type Celsius = Degrees;
type Degrees = Celsius;
main.pep:1:6: error: type alias 'Celsius' refers to itself
1 | type Celsius = Degrees;
| ^~~~~~~
main.pep:2:6: note: through type alias 'Degrees', declared here
2 | type Degrees = Celsius;
| ^~~~~~~
Traits and extensions
Pepper has no inheritance. Instead, you add behavior to a type with an
extension, and describe a set of behavior that several types share with a
trait. Extensions can be written anywhere, so you can add methods to a
library type, such as String, from your own code.
A trait is also a type: a value of type Shape holds any struct, or enum
whose cases carry values, that has the Shape trait, and calls its methods (see Traits as types).
Extensions
An extend block adds methods to a struct declared elsewhere, whether in the
same file, another file, or the standard library:
struct Temperature {
var celsius: float = 0.0;
construct(this.celsius = 0.0) {}
}
# `extend` adds methods to a struct declared elsewhere.
extend Temperature {
fahrenheit(): float {
return celsius * 9.0 / 5.0 + 32.0;
}
static freezing(): Temperature {
return Temperature(0.0);
}
bool ==(Temperature other) {
return celsius == other.celsius;
}
}
# Library types can be extended too.
extend String {
exclaim(): String {
var s = String();
s.append(this);
s.append("!");
return s;
}
}
# One specialization of a generic library type can be extended, and its
# methods can loop over `this`.
extend Array<int> {
total(): int {
var sum = 0;
for (var x in this) {
sum += x;
}
return sum;
}
}
struct Box<T> {
var value: T;
}
# Extending the generic struct adds the method to every Box<...>.
extend Box<T> {
get(): T {
return value;
}
}
main(): int {
var t = Temperature(100.0);
assert(t.fahrenheit() == 212.0, "extension method");
assert(Temperature.freezing() == Temperature(0.0), "static method and operator from an extension");
assert("hello".exclaim() == "hello!", "String extension");
var numbers = Array<int>();
numbers.append(2);
numbers.append(3);
assert(numbers.total() == 5, "Array<int> extension loops over this");
var b = Box<int>();
b.value = 5;
assert(b.get() == 5, "Box<int>");
var s = Box<String>();
s.value = "five";
assert(s.get() == "five", "Box<String>");
return 0;
}
- An extension can hold anything a struct body can hold except fields, a constructor, and a destructor: methods, static methods, operators, and indexers.
- Methods in an extension behave exactly like methods declared in the struct.
They can use
thisand name the struct’s fields directly, and they are called the same way. - A type can have any number of extensions.
- An extension method may share its name with the struct’s methods if its parameter types differ: it joins their overload set.
- An extension in another file is usable only where that file (or its module) is imported, and only if it is exported; the standard library’s extensions are usable everywhere. See Visibility.
Extending generic structs
extend Box<T>, using the struct’s own type parameter name, adds the methods
to every specialization: Box<int>, Box<String>, and so on. The example
above does this.
When Box constrains its parameter (struct Box<T: Shape>), the extension
keeps the constraint without restating it; extend Box<T: Shape> may
restate it as documentation (see constraints).
A constraint the struct does not have limits the extension to the
specializations that meet it: extend Box<T: Named> { ... } adds its
methods to Box<Square> but not to Box<String>, and
extend Box<T: Shape> with Shape makes a Box of shapes a shape (see
constrained extensions).
extend Box<int> targets one specialization: its methods exist only on
Box<int>, and calling one on a Box<String> is an error
(no member named 'doubled' in 'Box<String>').
A library type’s specialization can be extended the same way. The example’s
extend Array<int> adds total(), which loops over this with for.
An extension cannot add fields; declare every field in the struct itself.
Traits
A trait names a set of methods. Declare one with trait and method
signatures ending in ;. Then declare that a type has the trait with
extend Type with Trait, and provide the methods in that extension:
# A trait names a set of methods that a type provides.
trait Shape {
area(): int;
name(): String;
# A method with a body is a default: every type with the trait gets it.
describe(): String {
return "a " + name();
}
}
struct Square {
var side: int = 1;
construct(this.side = 1) {}
}
# `extend ... with` declares that Square has the Shape trait, and adds the
# trait's methods to it.
extend Square with Shape {
area(): int {
return side * side;
}
name(): String {
return "square";
}
}
# A trait can be generic, and can declare static methods; `this` as a type
# stands for the type that has the trait.
trait From<T> {
static this from(T value);
}
extend Square with From<int> {
static Square from(int side) {
return Square(side);
}
}
# A trait whose methods all have defaults needs no body: end the extension
# with `;`, the same as writing an empty `{}`.
trait Labeled {
label(): String {
return "shape";
}
}
extend Square with Labeled;
main(): int {
var sq = Square(3);
assert(sq.area() == 9, "trait method called on the struct");
assert(sq.name() == "square", "second trait method");
assert(sq.describe() == "a square", "default method from the trait");
assert(Square.from(4).area() == 16, "static method from a generic trait");
assert(sq.label() == "shape", "default method from a bodiless extension");
return 0;
}
- A trait can have type parameters, like
From<T>. - A trait can declare static methods. In a trait,
thisused as a type stands for the type that has the trait. extend Square with Shape, Printable { ... }attaches several traits at once.- When the type already has every method the traits need, from its own
methods or the traits’ defaults, the extension can end with
;instead of a body:extend Square with Labeled;is the same asextend Square with Labeled {}. Only an extension with awithclause can leave out its body;pep-fmtwrites an empty one as;. - A trait can declare several methods of one name with different parameter types; the type implements each with the method whose parameter types match (see Overloading).
- The methods in the extension are ordinary methods of the struct, so you
call them on a
Squareas usual. - A generic parameter can require a trait,
total_area<T: Shape>, so that only types declared to have it are accepted (see constraints). - The runtime declares traits too.
ToString(to_string(): String) makes a type usable in an interpolated string.
The compiler checks that the type has every method the trait declares, either in the extension or in the struct itself. A missing method is an error at the extension, with a note at the trait’s method:
main.pep:9:8: error: 'Square' does not implement 'area', required by trait 'Shape'
9 | extend Square with Shape {
| ^~~~~~
main.pep:2:5: note: 'area' declared here
2 | area(): int;
| ^~~~
A method that implements a trait should name its parameters as the trait does, since a call through a value of the trait type names its arguments with the trait’s names. A different name is a warning, whose fix renames the parameter and its uses:
main.pep:8:10: warning: parameter 'factor' of 'area' is named 'scale' in trait 'Shape'
8 | area(factor: int): int { return side * side * factor; }
| ^~~~~~
main.pep:2:5: note: 'area' declared here
2 | area(scale: int): int;
| ^~~~
Default method bodies
A trait method can have a body. A type that has the trait and does not define
that method gets the trait’s body as its own method; a type that defines the
method keeps its own. Inside a default body, the trait’s type parameters and
this stand for the types named in the with clause:
trait Shape {
area(): int;
sides(): int { return 4; }
}
extend Square with Shape {
area(): int { return side * side; }
}
Here Square(3).sides() returns 4.
A default method can have type parameters of its own (see
Generic methods). The runtime’s Iterator<T>
uses this for its map<U> and filter adapters,
which every type that declares Iterator<T> gets.
A default body sees only what its trait declares: a bare call to one of the
trait’s methods calls it on this, and any other bare name means what it
means where the trait is declared (a top-level function or variable), even
when a conforming type happens to have a method or field of that name. Call
through this explicitly only for the trait’s own methods.
A closure in a default body can use this and the trait’s methods too. Each
type that gets the default gets its own copy of the closure, which captures
this as that type, as a closure in one of its own methods would (see
this in methods):
# A closure in a trait's default method can use `this` and the trait's
# methods.
trait Shape {
area(): int;
# Every type with Shape gets this method, and its own copy of the
# closure, which captures `this`.
scaled_area(): (int) => int {
return (factor: int) => area() * factor;
}
}
struct Square {
var side: int = 1;
construct(this.side = 1) {}
}
extend Square with Shape {
area(): int {
return side * side;
}
}
main(): int {
var square = Square(3);
var scale = square.scaled_area();
assert(scale(2) == 18, "closure in a default method");
# The closure keeps the square alive and sees its current side.
square.side = 4;
assert(scale(2) == 32, "sees the current value");
return 0;
}
Numbers, bool and enums
A number type (int, long, byte, char, float, double and the
rest), bool, or an enum whose cases carry no values can declare a trait
too, with an extension written the same way. Inside its methods, this is
the value:
trait Describe {
describe(): String;
# A default method works for value types too.
shout(): String { return describe() + "!"; }
}
trait Zero {
static zero(): this;
}
# A number type conforms like a struct; `this` is the value.
extend int with Describe {
describe(): String { return this < 0 ? "negative" : "non-negative"; }
}
extend int with Zero {
static zero(): int { return 0; }
}
enum Light {
Red,
Green,
}
# An enum whose cases carry no values can conform too.
extend Light with Describe {
describe(): String { return this == Light.Red ? "stop" : "go"; }
}
# Each call is resolved for the type argument: no trait value is made.
describe_all<T: Describe>(items: Array<T>): String {
var text = "";
for (var item in items) {
text = text + item.describe() + " ";
}
return text;
}
zero_of<T: Zero>(): T { return T.zero(); }
main(): int {
var numbers = Array<int>();
numbers.append(-1);
numbers.append(2);
assert(describe_all(numbers) == "negative non-negative ", "numbers");
assert(Light.Green.shout() == "go!", "enum");
assert((5).describe() == "non-negative", "a call on a value");
assert(zero_of<int>() == 0, "a static member through T");
return 0;
}
-
The type then meets the constraint
T: Describe(see constraints), and its methods can be called on a value (x.describe()) or, for a static one, on the type (int.zero(), orT.zero()in a generic). Each call goes straight to the method: there is no object and no vtable. -
It is never a trait value. A number or such an enum cannot be stored in a
Describevariable, passed as one, or added to anArray<Describe>:main.pep:7:15: error: 'int' cannot be converted to trait 'Doubled': only structs and enums whose cases carry values can be trait values 7 | main(): int { Doubled d = 3; return 0; } | ^~~~~~~ main.pep:4:8: note: 'int' conforms to 'Doubled' here, which makes it meet the constraint, not a trait value 4 | extend int with Doubled { | ^~~ -
An extension of a number type may also add methods without a trait (
extend int { ... }), and operators for a number on the left of another type (extend double { *(v: Vector): Vector { ... } }makes2.0 * vwork; see A number on the left). An enum declares its methods inside the enum, so an extension of one must name a trait. -
The spellings of one type are one type:
extend intis alsoextend i32.
Traits as types
A trait’s name is also a type. A variable, parameter, field, return value, or
collection element of type Shape holds any struct, or enum whose cases
carry values, that declares Shape, and calling a method on it runs that
type’s own method:
# A trait used as a type holds any struct that declares the trait.
trait Shape {
area(): int;
name(): String;
}
struct Square {
var side: int = 1;
construct(this.side = 1) {}
}
extend Square with Shape {
area(): int { return side * side; }
name(): String { return "square"; }
}
struct Rect {
var width: int = 1;
var height: int = 1;
construct(this.width = 1, this.height = 1) {}
}
extend Rect with Shape {
area(): int { return width * height; }
name(): String { return "rect"; }
}
# One function accepts every Shape; each call runs the value's own method.
measure(shape: Shape): int {
return shape.area();
}
main(): int {
# A Square converts to a Shape wherever a Shape is expected.
var s: Shape = Square(3);
assert(measure(s) == 9, "square through a trait parameter");
assert(s.name() == "square", "method of the concrete type");
# Collections can hold different types that share a trait.
var shapes = Array<Shape>();
shapes.append(Square(2));
shapes.append(Rect(2, 5));
var total = 0;
for (var shape in shapes) {
total = total + shape.area();
}
assert(total == 14, "sum of areas");
# Two trait values are equal when they refer to the same object.
var same: Shape = s;
assert(same == s, "same object");
return 0;
}
-
Conversion is implicit. A
Squarebecomes aShapewherever aShapeis expected: an initializer, an assignment, an argument, a return value. -
Conformance is declared. Only a type that declares the trait with
extend Type with Traitconverts. Having the right methods is not enough:main.pep:11:20: error: 'Circle' does not conform to trait 'Shape' 11 | var s: Shape = Circle(); | ^~~~~~ main.pep:6:8: note: declare it with 'extend Circle with Shape' 6 | struct Circle { | ^~~~~~ -
A trait value is a shared reference to the struct, like any other struct value (see Memory). When the last reference goes away, whether it was a
Shapeor aSquare, the struct’s own destructor runs. -
Only instance methods are callable through the trait. Static methods, generic methods, and methods whose signature uses
thisas a type, need the concrete type; calling one through a trait value is an error. -
Methods must match the trait’s signature. An implementation with different parameter or return types is an error. The one exception is a trait method that returns a trait type: the implementation may return a struct that declares that trait, as
iterate()does in Iteration. -
==and!=on trait values compare identity: they are equal when they refer to the same object. -
copycopies the struct behind a trait value and returns it as the same trait type (see Copying trait values). It panics when that struct cannot be copied.
Converting a trait value back
A trait value remembers the struct it holds, so it converts back to that struct, or to another trait the struct declares. A type pattern names the type, like an enum case, and binds the value as that type:
# A trait value converts back to the struct it holds, or to another trait
# that struct declares, with `is`, `match` and `as?`.
trait Shape {
area(): int;
}
trait Named {
name(): String;
}
struct Square {
var side: int = 1;
construct(this.side = 1) {}
}
extend Square with Shape {
area(): int { return side * side; }
}
extend Square with Named {
name(): String { return "square"; }
}
struct Rect {
var width: int = 1;
var height: int = 1;
construct(this.width = 1, this.height = 1) {}
}
extend Rect with Shape {
area(): int { return width * height; }
}
# A type pattern tests for a struct and binds the value as that struct.
describe(shape: Shape): String {
return match (shape) {
Square(square) => $"a square of side {square.side}",
Rect(rect) => $"a {rect.width} by {rect.height} rectangle",
else => "another shape",
};
}
main(): int {
var shape: Shape = Square(3);
assert(describe(shape) == "a square of side 3", "match on the struct");
assert(describe(Rect(2, 5)) == "a 2 by 5 rectangle", "another struct");
# `is` tests without converting; as an `if` condition it can bind.
assert(shape is Square, "a square");
assert(!(shape is Rect), "not a rectangle");
if (shape is Square(square)) {
assert(square.side == 3, "bound as a Square");
}
# `as?` gives an optional: Empty when the value is something else.
var square = shape as? Square;
assert(square.value().side == 3, "converted to Square");
var rect = shape as? Rect;
assert(rect.is_empty(), "not a Rect");
# Converting to another trait works when the struct declares both.
var named = shape as? Named;
assert(named.value().name() == "square", "a Square is Named");
var other: Shape = Rect(1, 1);
assert((other as? Named).is_empty(), "a Rect is not Named");
return 0;
}
-
value is Squaretests whether the value holds aSquare. As the whole condition of aniforwhile,value is Square(square)also binds it, as for enums. -
match (value) { Square(square) => ..., Rect(rect) => ..., else => ... }picks the arm of the value’s struct. Any struct may declare a trait, so amatchexpression on a trait value needs anelsearm. -
value as? Squareconverts the value, givingSquare?: Empty when the value holds something else, or is null.aswithout the?is an error for a trait value, since the conversion can fail. -
Another trait works the same way:
value as? Namedandvalue is Namedsucceed when the struct behind the value declaresNamedtoo. -
A generic struct is named with its type arguments:
Cell<int>(cell). -
A value struct’s trait value holds a copy of it (see boxing), and converting back copies it out again.
-
The bound value is another reference to the same object, like any copy (see Memory); a test holds no reference after it.
-
Naming a struct that does not declare the trait is an error, since the value can never hold one:
main.pep:9:18: error: 'Point' does not implement 'Shape', so a 'Shape' value never holds one 9 | if (shape is Point) { | ^~~~~
Weak references and atomics
Weak<Shape> is a weak reference to a
trait value: it holds the struct’s weak reference and its vtable, and a
with block upgrades it to a Shape while a strong reference is left:
# A weak reference can point at a trait value: it keeps the struct behind
# it without keeping it alive.
trait Shape {
area(): int;
}
struct Square {
var side: int = 1;
construct(this.side = 1) {}
}
extend Square with Shape {
area(): int { return side * side; }
}
main(): int {
var shape: Shape = Square(3);
var weak = Weak<Shape>(shape);
# While a strong reference is left, `with` reaches the value as a Shape.
with (var live = weak) {
assert(live.area() == 9, "upgraded to a Shape");
} else {
assert(false, "still alive");
}
# Once the last strong reference goes, the Square is destroyed.
shape = __null;
assert(weak.expired(), "destroyed");
with (var live = weak) {
assert(false, "expired: the body is skipped");
}
return 0;
}
Atomic<Shape> is ruled out. Atomic<T> needs a thread-safe T, and
a trait value may hold any struct, thread-safe or not; it is also two
pointers wide, more than one atomic instruction swaps on every platform.
The error is the one for any type that is not thread-safe:
main.pep:8:32: error: 'Shape' does not conform to 'Shared', required by 'T' in 'Atomic'
8 | var current = async.Atomic<Shape>();
| ^~~~~
To share a trait value between threads, keep it in a
Mutex<Shape> and swap it with
replace, or use Atomic<Square> for one [Shared] struct type.
Iteration
A for loop visits each element of a collection in turn. You can loop over an
Array, over the code points of a
String, over a range of numbers (0..count),
and over any type of your own that follows the iterator protocol.
Looping over an array
for (var x in source) { ... } runs the body once per element, with x bound
to the element:
struct Item {
int value;
construct(this.value = 0) {}
}
make_items(): Array<Item> {
var items = Array<Item>();
items.append(Item(20));
items.append(Item(22));
return items;
}
main(): int {
var nums = Array<int>();
nums.append(1);
nums.append(2);
nums.append(39);
# The basic loop: x takes each element in order.
var total = 0;
for (var x in nums) {
total = total + x;
}
assert(total == 42, "sum of the elements");
# The loop variable may be typed, and the parentheses are optional.
long wide = 0;
for (long x in nums) {
wide = wide + x;
}
for var x: int in nums {
wide = wide - x;
}
assert(wide == 0, "typed loop variables");
# break and continue work as in a while loop.
var odd = 0;
for (var x in nums) {
if (x % 2 == 0) {
continue;
}
if (x > 10) {
break;
}
odd = odd + x;
}
assert(odd == 1, "only 1 is odd and below 10");
# A struct element is shared: changing a field changes the element,
# but rebinding the loop variable does not touch the array.
var items = make_items();
for (var item in items) {
item.value = item.value * 10;
item = Item(99);
}
assert(items[0].value == 200, "first element was updated");
assert(items[1].value == 220, "second element was updated");
# Iterating a temporary keeps it alive for the whole loop.
var sum = 0;
for (var item in make_items()) {
sum = sum + item.value;
}
assert(sum == 42, "temporary array stays alive");
# The loop is shorthand for driving the iterator yourself.
var it = nums.iterate();
var manual = 0;
while (it.next()) {
manual = manual + it.current();
}
assert(manual == 42, "the same elements, by hand");
# Modifying the array after the loop is fine.
nums.append(100);
assert(nums.count == 4, "appended after the loop");
return 0;
}
The example shows each part of the loop:
- The loop variable can be declared with
varor with a type:for (var x in nums),for (long x in nums), andfor (var x: int in nums)all work. - The parentheses are optional, as with
ifandwhile:for var x in nums { ... }. The braces are required. breakandcontinuework as they do in awhileloop.- The loop variable is a copy, like any
var. Assigning a new value to it does not change the collection. For a struct element, though, the copy is another reference to the same object (see Memory and reference counting), soitem.value = ...does change the element. - A temporary source stays alive. The loop holds a reference to what it
iterates, so
for (var item in make_items())is safe.
Looping over a string
A String yields its code points one at a time. Each one is a String holding
a single code point, the same value that indexing gives you (word[i]):
main(): int {
var word = "héllo ✓";
# A String yields one code point at a time, as a one-code-point String.
var count = 0;
var copy = "";
for (var c in word) {
assert(c.length == 1, "each piece is one code point");
copy.append(c);
count = count + 1;
}
assert(count == 7, "7 code points");
assert(copy == word, "the pieces rebuild the string");
# Each piece is the same value as indexing the string.
var i = 0;
for (var c in word) {
assert(c == word[i], "same as word[i]");
i = i + 1;
}
# An empty string runs the body zero times.
for (var c in "") {
assert(false, "never runs");
}
return 0;
}
Looping over a range
a..b is the numbers from a up to, but not including, b, and a..>b
includes b. Looping over one is how you count:
main(): int {
# `a..b` counts from a up to, but not including, b.
var total = 0;
for (var i in 0..5) {
total += i;
}
assert(total == 10, "0 + 1 + 2 + 3 + 4");
# `a..>b` includes b.
var product = 1;
for (var i in 1..>5) {
product *= i;
}
assert(product == 120, "5 factorial");
# The bounds give the type: `count` is a ulong, so i is too.
var names = ["Ada", "Grace", "Edsger"];
ulong letters = 0;
for (var i in 0..names.count) {
letters += names[i].size;
}
assert(letters == 14, "letters in every name");
# A range that starts past its end is empty.
for (var i in 3..1) {
assert(false, "never runs");
}
# A range is a value: test membership, or use the iterator adapters.
var digits = 0..10;
assert(digits.contains(9) && !digits.contains(10), "half-open");
var evens = 0;
for (var even in (1..>10).filter((n) => n % 2 == 0)) {
evens += even;
}
assert(evens == 30, "2 + 4 + 6 + 8 + 10");
return 0;
}
-
The bounds give the type. A range’s element type is inferred from its bounds like a generic call’s type argument, so
0..names.countcounts inulong. The bounds must be integers; anything else is an error at the operator:main.pep:2:22: error: range bounds must be integers, not 'String' 2 | for (var c in "a".."z") { | ^~ -
Precedence. A range binds more loosely than arithmetic and shifts, and more tightly than comparisons, so
0..n + 1is0..(n + 1). Two range operators in a row (1..2..3) are an error; add parentheses....is not a range operator. -
An empty range runs no iterations. A range whose start is past its end (
3..1,5..>4) is empty, anda..ais too. A closed range ending at its type’s largest value (250..>topwith abytetopof 255) stops there without overflowing. -
A range is a value, a
Range<T>. It hascontains(x)and theis_emptyproperty, and it is anIterator<T>, so it has the adapters (map,filter,enumerate,zip) andto_array(). Looping over one allocates nothing; the optimizer turns it into a plain counting loop.
The iterator protocol
The for loop is not tied to Array and String. It works with any type
that has the right methods, described by two traits in the runtime
(runtime/iterator.pep, documented in the Iterators
reference):
export trait Iterator<T> {
next(): bool; # advance; true if there is a current element
current(): T; # the element next() just moved to
}
export trait Iterable<T> {
iterate(): Iterator<T>;
}
- An iterator steps through elements.
next()moves to the next element and returnstrue, or returnsfalsewhen there are no more (and keeps returningfalseafter that).current()returns the elementnext()moved to. - An iterable is a collection that hands out a fresh iterator from
iterate().Array<T>returns anArrayIterator<T>,Stringreturns aStringIterator, andHashMap<K, V>andHashSet<K>return aHashMapIterator<K, V>and aHashSetIterator<K>.
for (var x in source) { body } behaves exactly like this:
{
var it = source.iterate(); # or `var it = source;` when source is itself an iterator
while (it.next()) {
var x = it.current();
{ body }
}
}
The loop chooses by the methods the source’s type has, like a C++ range-based
for: it uses iterate() if the type has one, and otherwise uses the source
itself as the iterator if it has both next() and current(). A type with
neither is a compile error:
main.pep:4:19: error: type 'Foo' is not iterable: implement Iterable<T> (iterate()) or Iterator<T> (next() and current())
4 | for (var x in f) {
| ^
HashMap<K, V> yields each entry as a tuple (K, V), so
for (var (name, age) in ages) names both parts; HashSet<K> yields its
members. Both visit entries in no particular order. Changing a value
(ages[name] = … for a key already present) is fine while looping, but adding
or removing an entry stops the program at the iterator’s next step.
Adapters
Iterator<T> has default methods that wrap an iterator in another one.
Each works lazily, one element at a time as the loop asks for it:
map(transform)yieldstransform(x)for each elementx; its element type is whatevertransformreturns.filter(keep)yields only the elements for whichkeep(x)istrue.enumerate()yields(index, x)tuples, counting from 0 inulong;enumerate<int>()counts inintinstead.zip(other)yields(x, y)pairs, one element from each iterator in step, and stops when either runs out.
They return iterators, so they chain, and a for loop can use the result
directly; destructure a tuple to name its parts. to_array() runs an
iterator to the end and collects what is left into a new Array:
struct Person {
String name;
int age;
construct(this.name = "", this.age = 0) {}
}
main(): int {
var people = Array<Person>();
people.append(Person("Ada", 36));
people.append(Person("Bo", 12));
people.append(Person("Cyd", 40));
# filter keeps the adults, map turns each one into a name.
var names = Array<String>();
for (var name in people.iterate().filter((p) => p.age >= 18).map((p) => p.name)) {
names.append(name);
}
assert(names.count == 2, "two adults");
assert(names[0] == "Ada", "Ada first");
assert(names[1] == "Cyd", "then Cyd");
# Adapters work on any Iterator, including a String's.
var kept = 0;
for (var c in "hello".iterate().filter((c) => c != "l")) {
kept += 1;
}
assert(kept == 3, "h, e, o");
# enumerate pairs each element with its position, and zip pairs two
# iterators in step; destructure the tuple to name both parts.
var ranked = "";
for (var (i, person) in people.iterate().enumerate()) {
ranked += $"{i}:{person.name} ";
}
assert(ranked == "0:Ada 1:Bo 2:Cyd ", "numbered from 0");
var seats = ["window", "aisle"];
var seated = 0;
for (var (person, seat) in people.iterate().zip(seats.iterate())) {
seated += 1; # Cyd gets no seat: zip stops at the shorter
}
assert(seated == 2, "two pairs");
# to_array collects what an iterator yields.
var ages = people.iterate().map((p) => p.age).to_array();
assert(ages.count == 3 && ages[2] == 40, "every age");
return 0;
}
Every type that declares Iterator<T> gets them: ArrayIterator<T> (from
Array.iterate()), StringIterator, ranges, and
your own iterators once you write extend YourType with Iterator<T>;. The adapter holds its own copy of
the iterator it wraps (the runtime’s iterators are
value structs, boxed when they become an Iterator<T>),
so a chain built from a temporary stays alive while the loop runs, and
it.map(f) and it.to_array() leave it where it was. They are generic methods (see
Generics), so they are not callable through an
Iterator<T> trait value.
Reading files and directories
Input that can fail partway, such as a directory listing or a file read line
by line, yields a Result per element: an IoResult<T> from
the io module. Handle each error where it
happens; there is nothing to check after the loop.
import [File, Directory, Path] from io;
main(): int {
var dir = Directory.temp().join("pepper-iteration-streams-example");
Directory.remove_all(dir);
assert(Directory.create_all(dir).is_ok(), "created the folder");
assert(File.write_text(dir.join("notes.txt"), "first\nsecond\r\nthird").is_ok(), "wrote notes");
assert(File.write_text(dir.join("todo.txt"), "").is_ok(), "wrote todo");
# A Directory yields its entries one at a time, each an IoResult<DirEntry>.
var names = 0;
for (var entry in Directory.open(dir).value()) {
if (entry.is_err()) {
print(entry.error().message);
break;
}
assert(entry.value().is_file, "both entries are files");
names += 1;
}
assert(names == 2, "two entries");
# A File's lines() yields each line, without its line ending.
var file = File.open(dir.join("notes.txt")).value();
var lines = Array<String>();
for (var line in file.lines()) {
lines.append(line.value());
}
assert(lines.count == 3 && lines[1] == "second", "three lines, \\r\\n removed");
# Adapters work on streams too: number the lines.
var again = File.open(dir.join("notes.txt")).value();
var last = "";
for (var (i, line) in again.lines().enumerate()) {
last = $"{i}: {line.value()}";
}
assert(last == "2: third", "the last line, numbered");
assert(Directory.remove_all(dir).is_ok(), "cleaned up");
return 0;
}
- A
Directoryis iterable.for (var entry in dir)reads the listing one entry at a time (Directory.iterate()returns aDirectoryIterator), without building an array the waydir.entries()does. Each element is anIoResult<DirEntry>. A directory that cannot be read yields one error and then ends. lines()yields each line of an openFile, or of any otherio.Readersuch as a socket, from what it reads next, as anIoResult<String>without the\nor\r\n. The last line counts even without a line ending. A read error, or a line that is not valid UTF-8, is yielded as an error and ends the lines.chunks(size)yields the rest of aReaderin arrays of at mostsizebytes, onereadeach, asIoResult<u8[]>s.- An error ends the sequence: after an
Errelement,next()returnsfalse. listener.incoming()accepts connections on aTcpListener, yielding each one as anIoResult<TcpStream>. It is the exception to the rule above: a failed accept is yielded and the loop goes on, since the next connection may succeed. It ends only once the listener is closed, so a server loopbreaks orreturns when it is done.- Leaving early cleans up. The listing is closed when the loop ends,
including by
breakorreturn, because the iterator is released then.
Writing your own iterator
To make your own type work in a for loop, give it next() and current()
(an iterator), or give it an iterate() method that returns such a type (an
iterable):
# An iterator: next() advances, current() returns the element.
struct Countdown {
int remaining;
int now;
construct(this.remaining = 0) {}
bool next() {
if (remaining == 0) {
return false;
}
now = remaining;
remaining = remaining - 1;
return true;
}
int current() {
return now;
}
}
# Declaring the trait documents the intent; the loop goes by the methods.
extend Countdown with Iterator<int>;
# An iterable: iterate() hands out a fresh iterator for each loop.
struct Evens {
int limit;
construct(this.limit = 0) {}
EvensIterator iterate() {
var it = EvensIterator();
it.limit = limit;
it.now = -2;
return it;
}
}
struct EvensIterator {
int limit;
int now;
bool next() {
now = now + 2;
return now < limit;
}
int current() {
return now;
}
}
extend EvensIterator with Iterator<int>;
extend Evens with Iterable<int>;
# A generic function can loop over any type that supports the protocol.
sum<C>(C values): int {
var total = 0;
for (var v in values) {
total = total + v;
}
return total;
}
main(): int {
var total = 0;
for (var n in Countdown(3)) {
total = total + n;
}
assert(total == 6, "3 + 2 + 1");
# A loop over an iterator in a variable uses that object up.
var c = Countdown(2);
for (var n in c) {
total = total + n;
}
assert(!c.next(), "the countdown is exhausted");
# An iterable starts a new pass every time.
var evens = Evens(10);
assert(sum<Evens>(evens) == 20, "0 + 2 + 4 + 6 + 8");
assert(sum<Evens>(evens) == 20, "a second pass sees the same elements");
var nums = Array<int>();
nums.append(30);
nums.append(12);
assert(sum<Array<int>>(nums) == 42, "arrays work too");
return 0;
}
Some things to keep in mind:
- Declaring the trait is optional. The loop looks only at which methods a
type has. Writing
extend Countdown with Iterator<int>;documents your intent, and the compiler then checks thatCountdownhas bothnext()andcurrent()(see Traits and extensions). Declaring it also lets the type be used as anIterator<int>value, and aforloop over such a value calls the methods through the trait. It also gives the type the adapters (map,filter,enumerate,zip) andto_array(). - Looping over an iterator uses it up. When the source is itself an
iterator stored in a variable, the loop drives that variable in place, so
it is exhausted afterwards, and a loop left with
breakcan be resumed by looping over it again. Aconstvariable holding a value iterator cannot be looped over, sincenext()changes it. An iterable starts a new, independent pass every time you loop over it. - Generic code can loop over a type parameter; each specialization checks the protocol for its own type (see Generics).
- Calling
current()out of turn panics in the runtime’s iterators: before the firstnext()you getArrayIterator.current() before next(), and afternext()returnedfalseyou getArrayIterator.current() past the end. Your own iterators should follow the same rule.
Modifying a collection while you loop over it
Changing an Array or a String while a loop is iterating over it stops the
program with a panic, because the iterator could otherwise skip or repeat
elements, or read past the end:
var nums = Array<int>();
nums.append(1);
nums.append(2);
for (var x in nums) {
nums.append(x); # panics: Array modified during iteration
}
Every method that changes an Array counts, including storing an element with
nums[i] = v. For a String, append and grow count, and the message is
String modified during iteration. The check happens on the iterator’s next
step, so modifying the collection after the loop is finished is fine.
To change elements as you go, loop over an index with while and store through
it, or collect the changes in a second array and apply them after the loop.
Loop scope
The loop variable and anything declared in the loop body exist only inside the
loop. Each pass gets a new loop variable, and struct values declared in the
body are released at the end of every pass, whether the pass ends normally or
by continue, break, or return. Leaving the loop early also releases the
hidden iterator, so a destructor on a custom iterator runs at that point.
Using the loop variable or a body local after the loop is an error, the same as for any name that is out of scope:
main.pep:6:12: error: use of undeclared identifier 'i'
6 | return i;
| ^
The loop variable, or a local in the loop body, may have the same name as a variable declared before the loop. It hides the outer variable inside the loop, and the outer variable keeps its value afterwards.
Scoped blocks with with
A with block enters a scope object, binds what it hands out, and cleans up
when the object is destroyed. It plays the role of Python’s with and C#’s
using, but the cleanup is ordinary destruction: it runs through the same
reference counting as every other object (see
Memory and reference counting).
import async;
# A module-scope counter: const, and changed through an Atomic.
const open_sessions = async.Atomic<int>(0);
# A scope object: enter() hands out what the block works with, and the
# destructor calls exit() when the last reference goes away.
struct Session {
var user: String;
construct(this.user = "") {
open_sessions.fetch_add(1);
}
destruct {
this.exit();
}
Session enter() {
return this;
}
exit() {
open_sessions.fetch_sub(1);
}
}
extend Session with Scoped<Session>;
# enter() may return a null reference: then the body is skipped and the
# `else` block runs.
struct Lookup {
var found: Session;
construct(Session session) {
found = session;
}
Session enter() {
return found;
}
exit() {}
}
extend Lookup with Scoped<Session>;
first_user(): String {
with (var session = Session("ada")) {
# `return` leaves the block too: the session still closes.
return session.user;
}
return "";
}
main(): int {
with (var session = Session("grace")) {
assert(open_sessions.load() == 1, "open inside the block");
assert(session.user == "grace", "bound to what enter() returned");
}
assert(open_sessions.load() == 0, "closed at the end of the block");
assert(first_user() == "ada", "the value was returned");
assert(open_sessions.load() == 0, "closed on return as well");
var hits = 0;
with (var s = Lookup(Session("linus"))) {
hits += 1;
} else {
hits -= 100;
}
with (var s = Lookup(__null)) {
hits -= 100;
} else {
hits += 1;
}
assert(hits == 2, "the body ran for a session, the else block for null");
return 0;
}
The header
with (var name = scope) { ... } else { ... }
with (Type name = scope) { ... }
with (scope) { ... }
nameis bound toscope.enter(), not toscopeitself. ASessioncan hand out itself; aWeak<T>hands out a strong reference to the object it points at, or null once that object is gone.- Without a binding, the block still holds the result of
enter()until it ends. Anenter()that returns nothing (declared without a return type) has nothing to bind, and binding it is an error. - The parentheses are optional, as with
if,whileandfor. The braces are required. namecannot be reassigned inside the block.
Skipping the body
When enter() returns a struct reference that is null, the body is skipped
and the optional else block runs instead. else is only allowed when
enter() returns a struct type, since nothing else can be null.
The Scoped<T> protocol
A type works in a with block when it has the methods of the Scoped<T>
trait (runtime/scoped.pep, documented in the Scoped
reference):
export trait Scoped<T> {
enter(): T; # what the header binds; null skips the body
exit(); # the cleanup; call it from the destructor
}
A with block goes by the methods a type has, not by the trait: a type
without enter() is a compile error in a with header. Declaring the trait
(extend Session with Scoped<Session>;) documents the intent, and the
compiler then checks that the type has both enter() and exit().
When exit() runs
The block keeps two locals: the scope object and the bound value. Both are
released on every path out of the block (the end of the body, return,
break and continue), the bound value first. Implementations call exit()
from their destructor:
destruct {
this.exit();
}
so exit() runs when the scope object’s last reference is released:
- A scope object created in the header (
with (var s = Session("grace"))) is released by the block, soexit()runs at the end of the block. - A scope object that something else also holds (a field, another local) lives
on after the block, and
exit()waits for its last owner. This is the same rule as any other object. - A copy of the bound value that outlives the block (stored in a field, returned) keeps that value alive, but not the scope object.
A panic stops the program without running destructors, so exit() does not
run then.
Error handling with Result
An operation that can fail returns a Result: either Ok, holding the value
you asked for, or Err, holding an error that says what went wrong. The
caller checks which one it got before using the value. Pepper has no
exceptions, so Result is how a function reports a failure the caller is
expected to handle. For a failure that should stop the program, such as a
broken invariant, use assert or panic instead.
Result and Error are part of the runtime, so you can use them
without an import. The Result reference lists every
member.
Creating and checking results
Result<TValue, TError> works like Rust’s Result<T, E>. It is an
enum with two cases, Ok(value: TValue) and Err(error: TError).
You build one with a case or with the static functions ok and err, and
read it with match, is,
or its methods:
# A function that can fail returns a Result instead of a plain value.
parse_digit(String s): Result<int> {
if (s == "0") { return Result<int>.ok(0); }
if (s == "1") { return Result<int>.ok(1); }
if (s == "2") { return Result<int>.ok(2); }
return Result<int>.err(Error("not a digit: " + s));
}
main(): int {
var good = parse_digit("2");
assert(good.is_ok(), "2 parses");
assert(!good.is_err(), "is_err is the opposite of is_ok");
assert(good.value() == 2, "value() returns the value");
var bad = parse_digit("x");
assert(bad.is_err(), "x does not parse");
assert(bad.error().message == "not a digit: x", "the error explains why");
# ok_or returns the value, or the fallback for an Err. It never panics.
assert(good.ok_or(-1) == 2, "Ok gives its value");
assert(bad.ok_or(-1) == -1, "Err gives the fallback");
# Result<int> is short for Result<int, Error>.
Result<int, Error> spelled_out = good;
assert(spelled_out.value() == 2, "the same type");
# Result is an enum: its cases build values too, and match reads them.
var parsed = Result<int>.Ok(7);
var doubled = match (parsed) { Ok(n) => n * 2, Err(e) => 0 };
assert(doubled == 14, "match binds the value");
if (bad is Err(e)) {
assert(e.message.length > 0, "is binds the error");
}
# An operation with no value to return uses void.
var done = Result<void>.ok();
var failed = Result<void>.err(Error("disk full"));
assert(done.is_ok() && failed.is_err(), "Result<void> works like any other");
return 0;
}
The pieces:
| Member | What it does |
|---|---|
Result<T>.Ok(value), Result<T>.ok(value) | Creates an Ok result holding value. |
Result<T>.Err(error), Result<T>.err(error) | Creates an Err result holding error. |
is_ok(), is_err() | Report which kind the result is. |
value() | Returns the value; panics with Result.value on an Err value if the result is an Err. |
error() | Returns the error; panics with Result.error on an Ok value if the result is Ok. |
ok_or(fallback) | Returns the value, or fallback if the result is an Err. Never panics. |
TError defaults to Error, so Result<int> means Result<int, Error>, and
values of the two spellings can be assigned to each other freely (the compiler
does generate a separate specialization for each spelling). Error is a struct
with one field, String message: Error("boom").message is "boom".
Use value() when an Err would be a bug in your program, since it stops the
program. When an Err is an expected outcome, match on the result, test it
with is (if (r is Err(e)) { ... }) or is_ok(), or supply a fallback with
ok_or.
Check before you read
A Result holds either its value or its error, never both. A match or an
is test reads the one it holds, so it cannot pick the wrong one. value()
and error() panic on the wrong one, so call value() only after checking
is_ok(), and error() only after checking is_err().
Result is a value enum: it is stored inline in
whatever holds it, so making one allocates nothing, and it is never null. A
slot of type Result that is never given a value holds its first case,
Ok, carrying the zero value of TValue (0, or null for a struct), so give
every result slot a value. Result<int>() is an error, since only the cases
make values.
Results with no value
An operation that has nothing to return on success, such as deleting a file,
uses void as its value type, like Result<(), E> in Rust. A void type
argument erases what it types, so in Result<void> the case Ok carries
nothing, ok() takes no argument, and value() returns nothing: calling it
only checks that the result is Ok. Using that call as a value, as in
var x = done.value();, is a compile error, as it is for any function that
returns void.
var done = Result<void>.Ok; # or Result<void>.ok()
var failed = Result<void>.err(Error("disk full"));
done.value(); # panics if `done` is an Err
match (failed) {
Ok => {}
Err(e) => { print(e.message); }
}
Custom error types and propagation
Any type can be the error type. A struct lets you carry details the caller can act on, not just a message. To pass an error up to your own caller, check the result and return early with its error; to change the error’s type on the way, build a new error from the old one:
# A custom error type: any type can be the second type argument.
struct ParseError {
String input;
int position;
construct(this.input = "", this.position = 0) {}
}
digit_value(String c): int {
var digits = "0123456789";
var i = 0;
while (i < 10) {
if (c == digits[i]) {
return i;
}
i = i + 1;
}
return -1;
}
parse_number(String s): Result<int, ParseError> {
if (s.length == 0) {
return Result<int, ParseError>.err(ParseError(s, 0));
}
var value = 0;
var position = 0;
for (var c in s) {
var d = digit_value(c);
if (d < 0) {
return Result<int, ParseError>.err(ParseError(s, position));
}
value = value * 10 + d;
position = position + 1;
}
return Result<int, ParseError>.ok(value);
}
# Propagate an error: check it, and return early with the same error.
add_numbers(String a, String b): Result<int, ParseError> {
var x = parse_number(a);
if (x.is_err()) {
return Result<int, ParseError>.err(x.error());
}
var y = parse_number(b);
if (y.is_err()) {
return Result<int, ParseError>.err(y.error());
}
return Result<int, ParseError>.ok(x.value() + y.value());
}
# Convert an error into another type on the way out.
check_total(String a, String b, int limit): Result<void> {
var sum = add_numbers(a, b);
if (sum.is_err()) {
var e = sum.error();
return Result<void>.err(Error("bad number '" + e.input + "'"));
}
if (sum.value() > limit) {
return Result<void>.err(Error("total is over the limit"));
}
return Result<void>.ok();
}
main(): int {
var sum = add_numbers("40", "2");
assert(sum.value() == 42, "40 + 2");
var bad = add_numbers("40", "4x2");
assert(bad.is_err(), "the second number is bad");
assert(bad.error().input == "4x2", "the error names the input");
assert(bad.error().position == 1, "and where it went wrong");
assert(check_total("1", "2", 10).is_ok(), "3 is within the limit");
var over = check_total("10", "20", 10);
assert(over.error().message == "total is over the limit", "30 is not");
var wrong = check_total("1", "", 10);
assert(wrong.error().message == "bad number ''", "an empty number is an error");
return 0;
}
The ? operator, below, writes each of these checks in one character.
Propagating errors with ?
? after a Result passes its error up, like Rust’s ?: on an Ok result
it is the value, and on an Err result it returns that error from the
enclosing function at once. var x = parse(s)?; does what the if (x.is_err()) { return ...err(x.error()); } checks above do:
# `?` propagates a failure: on an Err it returns that error from the
# enclosing function, and on an Ok it is the value.
struct ParseError {
String input;
construct(this.input = "") {}
}
parse_number(text: String): Result<int, ParseError> {
if (text.is_empty) {
return Result<int, ParseError>.err(ParseError(text));
}
var value = 0;
for (var c in text) {
var digit = "0123456789".find(c);
if (digit < 0) {
return Result<int, ParseError>.err(ParseError(text));
}
value = value * 10 + (digit as int);
}
return Result<int, ParseError>.ok(value);
}
# Each `?` returns early with the error, so the sum is computed only when
# both numbers parse.
add_numbers(a: String, b: String): Result<int, ParseError> {
return Result<int, ParseError>.ok(parse_number(a)? + parse_number(b)?);
}
# A function with a different error type: the `[Implicit]` constructor
# converts a ParseError on its way out.
struct ConfigError {
String message;
[Implicit]
construct(e: ParseError) {
this.message = "bad number '" + e.input + "'";
}
}
read_limit(text: String): Result<int, ConfigError> {
var limit = parse_number(text)?;
if (limit == 0) {
return Result<int, ConfigError>.err(ConfigError(ParseError(text)));
}
return Result<int, ConfigError>.ok(limit);
}
first_name(names: Array<String>): String? {
if (names.is_empty) {
return Optional<String>.Empty;
}
return Optional<String>.Value(names[0]);
}
# On an Optional, `?` returns an Empty optional from a function that returns
# one.
first_length(names: Array<String>): ulong? {
return Optional<ulong>.Value(first_name(names)?.length);
}
main(): int {
assert(add_numbers("40", "2").value() == 42, "40 + 2");
assert(add_numbers("40", "4x2").error().input == "4x2", "the error is the second number's");
assert(read_limit("10").value() == 10, "a limit");
assert(read_limit("ten").error().message == "bad number 'ten'", "converted to a ConfigError");
assert(first_length(["Ada", "Grace"]).value() == 3, "Ada");
assert(first_length(Array<String>()).is_empty(), "no names");
return 0;
}
The rules:
- The enclosing function must declare that it returns a
Result, or the compiler reports'?' requires the enclosing function to return a Result. A closure that uses?writes its return type out:(s: String): Result<int> => Result<int>.ok(parse(s)? + 1). - The error type must be the function’s error type, or convert to it
through an
[Implicit]constructor (see Implicit conversions), asConfigErrordoes above, like Rust’sFrom. Otherwise the error iscannot convert error type 'ParseError' to 'ConfigError'. - On a
Result<void>,?yields nothing, so it stands as a statement:remove(path)?;. - On an
Optional,?works the same way in a function that returns an optional: an Empty optional returns Empty (see Optional values). - The early return is an ordinary
return: locals are released andwithblocks are exited as for any other.
Each ? in a statement runs before the rest of the statement, so two
placements are errors:
- After a call in the same statement, as in
f() + g()?, whereg()would run beforef():'?' cannot follow a call in the same statement. Callf()in a statement of its own first. Reading variables and fields before a?is fine, and several?s run in the order written:add(parse(a)?, parse(b)?). - Where it might not run: the right operand of
&&or||, a branch of?:, or amatcharm. Use aniformatchstatement there.
A ? in a while condition runs on every iteration, before the condition
is tested.
Results from the io module
The io module is a good model of a library built
on Result. Every operation that can fail returns an IoResult<T>, a type
alias for Result<T, IoError>, and operations with nothing to return use
IoResult<void>. IoError holds the platform’s error code and a message.
To decide what to do about the failure, kind() returns the code’s portable
ErrorKind (NotFound, PermissionDenied, AlreadyExists, …,
Other), an enum to match on, and a predicate per kind, such as
is_not_found() and is_permission_denied(), tests for one of them:
import [File, Directory, Path, IoResult, IoError, ErrorKind] from io;
# IoResult<T> is Result<T, IoError>: the same methods, a richer error.
read_setting(Path path): String {
var text = File.read_to_string(path);
if (text.is_err()) {
if (text.error().is_not_found()) {
return "default";
}
return "error: " + text.error().message;
}
return text.value();
}
# kind() sorts the platform's code into an ErrorKind, for a match.
describe(error: IoError): String {
return match (error.kind()) {
NotFound => "missing",
PermissionDenied => "not allowed",
else => error.message,
};
}
main(): int {
var dir = Directory.temp().join("pepper-results-io-example");
var created = Directory.create_all(dir);
assert(created.is_ok(), "created the folder");
var path = dir.join("setting.txt");
assert(read_setting(path) == "default", "a missing file gives the default");
var missing = File.open(path);
assert(missing.is_err() && missing.error().kind() == ErrorKind.NotFound, "the kind of the error");
assert(describe(missing.error()) == "missing", "a match on the kind");
IoResult<void> written = File.write_text(path, "blue");
assert(written.is_ok(), "wrote the file");
assert(read_setting(path) == "blue", "the file's contents");
assert(Directory.remove_all(dir).is_ok(), "cleaned up");
return 0;
}
See io: errors and platform types for the full list of kinds and the platform codes each one covers.
A library that reads a file format reports damaged data the same way:
IoError.invalid_input(message) makes an error whose is_invalid_input()
holds, as compress and archive return for corrupt input.
Reading a directory or a file line by line in a for loop yields one
IoResult per element, so each error is handled where it happens; see
Iteration.
Optional values
A value that may be missing has the type Optional<T>, written T? for
short: either Value,
holding a T, or Empty, holding nothing. It plays the role of Rust’s
Option<T> and Swift’s Optional. Use it instead of a null reference or a
sentinel such as -1 or "": the type tells the caller that the value can be
absent, and reading it goes through a method that checks.
Optional is part of the runtime, so you can use it without an import. The
Optional reference lists every member.
# A lookup that may find nothing returns an Optional instead of a sentinel.
struct Person {
String name;
int age;
construct(this.name = "", this.age = 0) {}
}
find_adult(people: Array<Person>): Optional<Person> {
for (var p in people) {
if (p.age >= 18) { return Optional<Person>.Value(p); }
}
return Optional<Person>.Empty;
}
# `ulong?` is shorthand for `Optional<ulong>`.
index_of(text: String, wanted: String): ulong? {
var i: ulong = 0;
while (i < text.length) {
if (text[i] == wanted) { return Optional<ulong>.Value(i); }
i = i + 1;
}
return Optional<ulong>.Empty;
}
main(): int {
var people = Array<Person>();
people.append(Person("ada", 12));
assert(find_adult(people).is_empty(), "no adult yet");
people.append(Person("grace", 45));
var adult = find_adult(people);
assert(adult.is_value(), "grace is an adult");
# An optional is a condition by itself: `if (adult)` tests is_some().
if (!adult) { return 1; }
assert(adult.value().name == "grace", "value() returns the Person");
# Works for types that have no null, too.
assert(index_of("pepper", "e").value() == 1, "first 'e' is at 1");
assert(index_of("pepper", "z").is_empty(), "no 'z'");
# value_or supplies a fallback instead of panicking.
assert(index_of("pepper", "z").value_or(99) == 99, "fallback for Empty");
# Optional is an enum with the cases Empty and Value(value).
var nothing = Optional<int>.Empty;
assert(nothing.is_empty(), "the Empty case is empty");
var first_p = match (index_of("pepper", "p")) {
Empty => -1,
Value(at) => at as int,
};
assert(first_p == 0, "match on an optional");
# An optional slot that is not given a value starts as Empty.
var later: String?;
assert(later.is_empty(), "unassigned optionals are Empty");
return 0;
}
The T? shorthand
T? means Optional<T> wherever a type is written: x: int?,
find(key: String): Node?, Array<Node?>, a field Token? leaf;. The ?
applies to everything before it, so int[]? is an optional array, and
Array<int?> is an array of optional values.
A C-style declaration inside a function body, such as Node? n = m;, is read
as a declaration only when a name follows the ? and then =, ; or the end
of the line; a ? b : c stays a conditional expression.
Returning early with ?
In a function that returns an optional, ? after an optional is its value,
or, when it is Empty, returns Empty from the function at once, as ? does
with an error for Result:
middle_name(person: Person): String? {
var names = person.full_name?.split(" ");
if (names.count < 3) {
return Optional<String>.Empty;
}
return Optional<String>.Value(names[1]);
}
Using ? on an optional in a function that does not return an optional is
an error ('?' requires the enclosing function to return an Optional).
Optional slots start as Empty
A variable or field of an optional type that is not given a value starts as Empty, not null:
struct Node {
Token? leaf; # Empty until assigned
}
var count: int?; # Empty
Creating and checking optionals
| Member | What it does |
|---|---|
Optional<T>.Value(value) | Creates a Value optional holding value. |
Optional<T>.Empty | An Empty optional. |
is_value(), is_empty() | Report which case the optional is. |
!! (truth test) | An optional is a condition by itself: if (found) is if (found.is_value()), and !found is found.is_empty(). |
value() | Returns the value; panics with Optional.value on an Empty optional if the optional is Empty. |
value_or(fallback) | Returns the value, or fallback if the optional is Empty. Never panics. |
Use value() when an Empty optional would be a bug in your program, since it stops the
program. When a missing value is an expected outcome, test it first (if (found), is_value() or is_empty()), or supply a fallback with value_or.
Testing the optional itself works because Optional declares the truth-test
operator !! (see Truth tests).
Optional<T> works for every T, including types that have no null at all,
such as int or ulong. For a struct type, a Value optional holds a
reference to the object, like any other struct slot (see
Memory and reference counting).
Optional<T> is an enum with the cases Empty and
Value(value), so match and is work on it too:
var shown = match (name) {
Empty => "nobody",
Value(text) => text,
};
if (name is Value(text)) { print(text); }
An optional is a value enum: it lives inline in the
variable, field or array element that holds it, so creating one allocates
nothing, and copying one gives an independent optional. It is never null,
and has no identity: compare two optionals with ==, which compares
whether each holds a value and, if both do, their values; === on an optional
is an error.
Optionals in the standard library
Standard library lookups that can come up empty return an optional rather than a sentinel value:
| API | Returns | Empty when |
|---|---|---|
Array<T>.find(item) | ulong? | no element equals item |
io.Path.parent() | Path? | the path is a root (/, C:\), a bare name with no root (a, ., ..), or empty |
io.Path.file_name() | String? | the path is a root or empty, or its last component is . or .. |
io.Path.file_stem() | String? | there is no file name |
io.Path.extension() | String? | there is no file name, or the name has no dot after its first character (Makefile, .bashrc); foo. has the empty extension "" |
io.Path.dot_index(name) | ulong? | name has no dot after its first character |
http.Headers.get(name) | String? | no field has that name |
HashMap<K, V>.try_get(key) (and Map, OrderedMap) | V? | key is absent |
# Standard library lookups that may come up empty return an optional.
import io;
main(): int {
# Array.find returns `ulong?`: the index, or none.
var names = ["ada", "grace", "linus"];
var at = names.find("grace");
assert(at.value() == 1, "grace is at index 1");
assert(names.find("ken").is_empty(), "ken is not in the array");
# Path.parent, file_name, file_stem and extension return `Path?` or
# `String?`: none when the path has no such part.
var path = io.Path("docs").join("guide.md");
assert(path.file_name().value() == "guide.md", "file name");
assert(path.file_stem().value() == "guide", "stem");
assert(path.extension().value() == "md", "extension");
assert(path.parent().value() == io.Path("docs"), "parent");
assert(io.Path("/").parent().is_empty(), "a root has no parent");
assert(io.Path("notes").parent().is_empty(), "nor does a bare name");
assert(io.Path("a/..").file_name().is_empty(), "'..' is not a file name");
assert(io.Path("Makefile").extension().is_empty(), "no dot, no extension");
# value_or supplies a default when none is an expected outcome.
var ext = io.Path("Makefile").extension().value_or("txt");
assert(ext == "txt", "fallback extension");
return 0;
}
Breaking change. Array.find used to return a long that was -1 for
“not found”, and the Path methods above returned an empty Path or
String (or name.size for dot_index). Code written against the old
API must test the optional instead: a.find(x) >= 0 becomes
a.find(x).is_value(), p.parent().is_empty becomes
p.parent().is_empty(), and a value is read with value() or value_or.
A comparison such as a.find(x) == -1 still compiles but compares the
optional object rather than an index, so search for these and rewrite them.
String.find is unchanged and still returns -1.
Optional or Result
An Optional<T> says only that a value is missing. When the caller needs to
know why, return a Result instead, whose Err carries an
error.
Current limits
- Some runtime APIs still use null references, default values or sentinels
for “missing”: a
HashMap’sget(and indexer) returns the default value for a missing key (usetry_getfor an optional), andString.findreturns-1, for example.
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.
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";
Atomics
An atomic value can be read and updated by several threads at once without
a data race: every operation on it happens as one indivisible step. Pepper
provides this through async.Atomic<T>, the equivalent of C++
std::atomic<T>.
Threads themselves, and the rules for what they can share, are in Threads and shared data.
Atomic<T>
Atomic<T> belongs to the async module, with the other
types threads share: import async and write async.Atomic<T>, or bring
the name in with import [Atomic] from async. The
Atomic reference documents every method.
import async;
main(): int {
var a = async.Atomic<int>(41);
assert(a.load() == 41, "the initial value");
a.store(1);
assert(a.load() == 1, "store replaces the value");
# The fetch_ operations return the value from before the change.
assert(a.fetch_add(5) == 1, "fetch_add returns the old value");
assert(a.load() == 6, "1 + 5");
assert(a.fetch_sub(2) == 6, "fetch_sub returns the old value");
assert(a.load() == 4, "6 - 2");
var flags = async.Atomic<uint>(12); # 0b1100
flags.fetch_and(10); # 0b1000
assert(flags.load() == 8, "fetch_and");
flags.fetch_or(1); # 0b1001
assert(flags.load() == 9, "fetch_or");
flags.fetch_xor(9); # 0b0000
assert(flags.load() == 0, "fetch_xor");
# compare_exchange stores only if the current value is the expected one.
assert(a.compare_exchange(4, 10), "4 matched, so 10 is stored");
assert(!a.compare_exchange(4, 20), "10 does not match 4");
assert(a.load() == 10, "the failed exchange changed nothing");
# exchange always stores, and returns what was there.
assert(a.exchange(3) == 10, "exchange returns the old value");
assert(a.load() == 3, "and stores the new one");
assert(a.is_lock_free(), "an int fits a native atomic instruction");
return 0;
}
| Method | What it does |
|---|---|
Atomic<T>(value) | Creates an atomic holding value (zero, or null for a struct type, if you leave it out). |
load() | Reads the value. |
store(v) | Replaces the value with v. |
compare_exchange(expected, desired) | Stores desired only if the value is currently expected; returns whether it did. |
exchange(v) | Stores v and returns the value it replaced. |
fetch_add(v), fetch_sub(v) | Adds or subtracts v; returns the value from before. Overflow wraps around. |
fetch_and(v), fetch_or(v), fetch_xor(v) | Combines the value bitwise with v; returns the value from before. |
is_lock_free() | Reports whether the operations use native atomic instructions. |
Each operation compiles to a native atomic instruction. load has acquire
ordering, store has release ordering, and compare_exchange and the
fetch_ operations are acquire-release. exchange is a compare_exchange
retry loop.
Some details differ from C++:
compare_exchangedoes not updateexpectedwhen it fails. Callload()to get the current value before you retry.- The comparison is bitwise. For a floating-point
T,0.0and-0.0are different values. For a structT, it compares references (whether both are the same object), not the objects’ contents. - The
fetch_operations are for integer types. Calling one on a floating-point or structTcompiles, but stops the program when it runs. is_lock_free()is true for any number type of 8 bytes or less, and for any struct type.Tmust be thread-safe.Atomicis a[Shared]struct, so it is declaredAtomic<T: Shared>:Tis a number,bool, an enum, a[Shared]struct, or another type in What threads can share.Atomic<String>is a compile error atString(see theSharedconstraint). A tuple is thread-safe, but its value cannot be stored atomically, soAtomicof a tuple is an error too, and so isAtomicof a trait type (see Weak references and atomics).
Common patterns
import async;
# An Atomic can only hold a thread-safe struct: one declared [Shared], whose
# fields are const and thread-safe.
[Shared]
struct Config {
const version: int;
construct(this.version = 0) {}
}
# A module-scope Atomic that every function can share.
const next_id = async.Atomic<long>(0);
new_id(): long {
return next_id.fetch_add(1) + 1;
}
# The compare-and-swap retry loop: build any read-modify-write update.
double_it(async.Atomic<int> value) {
var current = value.load();
while (!value.compare_exchange(current, current * 2)) {
current = value.load();
}
}
main(): int {
assert(new_id() == 1, "first id");
assert(new_id() == 2, "second id");
var n = async.Atomic<int>(21);
double_it(n);
assert(n.load() == 42, "doubled");
# Copies of an Atomic share one slot, because Atomic is a struct.
var same = n;
same.store(7);
assert(n.load() == 7, "both names see the store");
# An Atomic can hold a struct reference.
var first = Config(1);
var current = async.Atomic<Config>(first);
assert(current.load().version == 1, "load returns the reference");
# compare_exchange compares references: the same object, not equal contents.
var lookalike = Config(1);
var second = Config(2);
assert(!current.compare_exchange(lookalike, second), "a different object");
assert(current.compare_exchange(first, second), "the same object");
assert(current.load().version == 2, "swapped");
var old = current.exchange(first);
assert(old.version == 2, "exchange hands back the old reference");
return 0;
}
- A shared counter.
fetch_add(1)hands out unique, increasing numbers, even when many threads call it at once. - A compare-and-swap loop. To apply any update that the
fetch_operations do not cover, load the current value, compute the new one, andcompare_exchange; if another thread changed the value in between, the exchange fails and you try again with a freshload(). - Sharing an Atomic.
Atomicis a[Shared]struct, so copying one copies a reference: every copy shares the same slot, on any thread. To share a value, pass theAtomicaround or keep it in a module-scope variable. - Atomic struct references. An
Atomic<T>whoseTis a[Shared]struct type holds a counted reference, and follows the same rules as any other reference (see Memory and reference counting):storeand a successfulcompare_exchangetake a reference to the new value and release the one they replace, andloadandexchangegive you your own reference. The swap in the slot is itself atomic, so concurrent stores never leak or release a value twice. When theAtomicitself is destroyed, it releases the reference it holds.
Atomics and threads
Atomic<T> is how threads share a value that changes. Keep it in a
module-scope variable, or in a const field of a [Shared] struct, and every
thread sees the same slot. Threads and shared data shows it
together with Thread.spawn.
Threads and shared data
The async module starts and manages operating-system threads. Pepper keeps
threads safe by controlling what they can share: only thread-safe values
can be reached from more than one thread, and the compiler checks the types
that claim to be.
On WebAssembly (--target wasm32-wasip1) a program has one thread:
Thread.spawn panics. Mutexes, channels and atomics still work, as long as
nothing waits for another thread (see WebAssembly).
Starting and joining threads
import async;
const finished = async.Atomic<int>(0);
# A thread body is a function that takes nothing and returns nothing.
count_to_a_million() {
var me = async.Thread.current();
assert(me.name() == "counter", "a spawned thread sees its own Thread");
var total: long = 0;
var i = 0;
while (i < 1000000) {
total = total + 1;
i = i + 1;
}
finished.fetch_add(1);
}
main(): int {
var main_thread = async.Thread.current();
assert(main_thread.name() == "main", "the main thread is named main");
var first = async.Thread.spawn(count_to_a_million, "counter");
var second = async.Thread.spawn(count_to_a_million, "counter");
assert(first.id() != second.id(), "every thread has its own id");
first.join();
second.join();
assert(first.is_finished(), "join waits for the body to return");
assert(finished.load() == 2, "both bodies ran");
# How many threads can run at once, and on how many cores.
assert(async.logical_cpus() >= async.physical_cpus(), "hyperthreads count once in physical_cpus");
return 0;
}
Thread.spawn(body, name)starts a new thread that runsbody, a function of typeThreadBody(an alias forshared () => void), and returns itsThread. The name is optional.join()waits for the body to return. Afterjoin(), the joining thread sees everything the other thread did. A thread can be joined once.Thread.current()returns the calling thread’sThread. For a spawned thread it is the same object thatspawnreturned. The main thread’s is namedmain.id()returns the thread’sThreadId, a number that no other thread in the program has. Ids come from the runtime, starting at 1, and are not the operating system’s thread ids. AThreadIdcan be compared and used as aHashMapkey.is_finished()reports whether the body has returned.is_joinable()reports whetherjoin()may be called.Thread.sleep(milliseconds)pauses the calling thread (in a task, only the task: see Tasks).- Dropping every reference to a
Threadwithout joining it detaches the thread. It keeps running, and its resources are freed when it finishes. - A
panicor failedasserton any thread ends the whole program.
The body can be a named function or a closure. A closure passes data to the thread through its captures:
var total = async.Atomic<long>(0);
var step: long = 5;
var worker = async.Thread.spawn(() => { total.fetch_add(step); });
worker.join();
The body is a shared function value,
so the closure may capture only thread-safe values (numbers, Atomic<T>,
[Shared] structs, shared function values). The compiler checks each
capture and reports one that is not thread-safe where the closure uses it:
main.pep:5:46: error: closure passed as 'shared () => void' captures 'name' of type 'String', which is not thread-safe
5 | var t = async.Thread.spawn(() => { print(name); });
| ^~~~
main.pep:4:9: note: capture a thread-safe value instead, such as an 'async.Atomic<T>' or a '[Shared]' struct
4 | var name = "worker";
| ^~~~
Every module-scope variable is const and thread-safe, so any thread may
use one (see below).
The Thread reference documents every method.
Processor counts
async.logical_cpus()returns how many threads the machine can run at the same moment. A core with hyperthreading (SMT) counts two or more times. This is a good default size for a pool of CPU-bound worker threads. In a Linux container with a CPU quota (docker run --cpus), it is the quota rounded up, when that is fewer (see Containers).async.physical_cpus()counts each physical core once. On Linux it reads the kernel’s CPU topology, on macOShw.physicalcpu, and on WindowsGetLogicalProcessorInformationEx. Inside a virtual machine it counts the cores the hypervisor presents, because the guest cannot see the host’s hardware.
Thread-local variables
A module-scope var marked [ThreadLocal] has a separate copy on every
thread, like C++ thread_local or a Rust #[thread_local] static:
import async;
import [Thread] from async;
# A separate `depth` on every thread, each starting at 0.
[ThreadLocal]
var depth: int = 0;
const deepest = async.Atomic<int>(0);
descend(levels: int) {
depth += 1;
if (depth > deepest.load()) {
deepest.store(depth);
}
if (levels > 1) {
descend(levels - 1);
}
depth -= 1;
}
work() {
descend(5);
}
main(): int {
depth = 100; # changes only the main thread's copy
var a = Thread.spawn(work);
var b = Thread.spawn(work);
a.join();
b.join();
assert(deepest.load() == 5, "each thread counted from its own 0");
assert(depth == 100, "the main thread's copy is its own");
return 0;
}
- Every thread’s copy starts from the initializer, which must be a
constant: a number,
boolorcharliteral (negated, or cast), an enum case, or nothing, for the type’s default (zero, or null for a struct). To start from anything else, leave the initializer out and set the value on the thread’s first use. - Reading or assigning one touches only the calling thread’s copy, so it is
a plain
var: it need not beconstlike other module-scope variables, and needs no atomics or locks. A read is a single load from the thread’s storage (a%fs-relative load on x86-64 Linux), not a call. - Its type must be thread-safe (see What threads can share):
a number, a raw pointer, a
[Shared]struct, asharedfunction type, or a tuple or[Value]struct of them. A value of any other type, such as aString, goes in aThreadLocal<T>. - When a thread started with
Thread.spawnexits (theasyncruntime’s workers and blocking threads included), each of its thread-locals is set to its default and the struct it held is released, last declared first. The main thread’s are never released, like every other module-scope variable, and a thread started by C code does not release its own. - A task sees the thread-locals of the worker thread it runs on, as with a
ThreadLocal<T>.
[ThreadLocal] applies only to a module-scope variable. A variable whose
type is not thread-safe, or whose initializer is not a constant, is
reported at the type or the initializer:
main.pep:3:14: error: thread-local variable 'scratch' has type 'Plain', which is not thread-safe
3 | var scratch: Plain;
| ^~~~~
note: a per-thread value of any type is an 'async.ThreadLocal<T>' (after 'import async')
main.pep:1:8: note: 'Plain' is not declared [Shared]
1 | struct Plain { var x: int; }
| ^~~~~
main.pep:3:18: error: thread-local variable 'depth' must start from a constant
3 | var depth: int = start();
| ^~~~~
note: every thread starts from this value; leave it out and assign the value on the thread's first use instead
Per-thread values with ThreadLocal
ThreadLocal<T> is a variable with a separate value on every thread, of
any type:
import async;
import [Thread, ThreadLocal] from async;
# One variable, a separate value on every thread.
const calls = ThreadLocal<int>();
const totals = async.Atomic<long>(0);
record_call() {
calls.set(calls.get() + 1);
}
work() {
record_call();
record_call();
record_call();
totals.fetch_add(calls.get()); # 3 on every thread
}
main(): int {
record_call();
var a = Thread.spawn(work);
var b = Thread.spawn(work);
a.join();
b.join();
assert(totals.load() == 6, "each thread counted its own calls");
assert(calls.get() == 1, "the main thread's value is its own");
return 0;
}
get()returns the calling thread’s value, or the default ofT(zero, or null for a struct) until that thread callsset.set(value)replaces only the calling thread’s value.is_set()andclear()test for and remove the calling thread’s value.- When a thread started with
Thread.spawnexits, its value is released, even if theThreadLocalitself was destroyed first. As with[ThreadLocal]variables, the main thread’s value is released only byclear(), and a thread started by C code leaks the values it did not clear. - Each thread keeps its values in a table of its own, held in
[ThreadLocal]variables, soget()costs a few loads and no call, and a program can have any number ofThreadLocals.
A ThreadLocal is usually a module-scope variable. Its T does not need to
be thread-safe, because each value stays on the thread that stored it. For a
T that is not thread-safe, such as a String, the value is reached like a
Mutex’s (see What a lock body may do):
with (var value = local.enter()) { ... }binds this thread’s value; theelseblock runs when the thread has none;get()is a compile error;set(value)takes a new value: a constructor call, a string literal,copy(x), ormove(x).
Otherwise a function called while a Mutex is locked could keep part of the
locked value in a thread-local, and a later get() would hand it out after
the lock was released. The
ThreadLocal reference has the details.
Tasks that take turns on one worker thread share its ThreadLocal values;
a TaskLocal<T> gives each task its own.
What threads can share
Every struct value is a counted reference (see Memory and reference counting). Updating a count from two threads at once can free an object too early or never, so by default a struct’s counts are plain, fast increments, and its objects must stay on one thread.
A struct declared [Shared] has atomic reference counts, so references
to one object can be copied and dropped on any thread:
import async;
# A struct that threads can share: declared [Shared], every field const and
# thread-safe.
[Shared]
struct Settings {
const port: int;
const retries: int;
construct(this.port = 0, this.retries = 0) {}
}
# async.Atomic<T> of a [Shared] struct: threads read the current settings, and one
# of them can replace them.
const current = async.Atomic<Settings>(Settings(8080, 3));
const seen = async.Atomic<long>(0);
read_settings() {
var settings = current.load();
seen.fetch_add(settings.port);
}
main(): int {
var readers = Array<async.Thread>();
var i = 0;
while (i < 4) {
readers.append(async.Thread.spawn(read_settings));
i = i + 1;
}
for (var reader in readers) {
reader.join();
}
assert(seen.load() == 4 * 8080, "every reader saw the settings");
current.store(Settings(9090, 5));
assert(current.load().port == 9090, "replaced");
return 0;
}
The compiler checks every [Shared] struct:
-
Every field must be
const. It is set once, by the constructor, and never assigned again, so threads cannot race on it. To change shared state, give the struct a thread-safe member such as anAtomic<T>. -
Every field must have a thread-safe type. These are all the thread-safe types, the same ones the constraint
T: Sharedaccepts:- every number type (
int,long,byte,char,short,float,doubleand the rest),bool, andvoid; - enums whose cases carry no values, which are stored as integers;
[Shared]structs, and enums whose cases carry values that are declared[Shared];- weak references to thread-safe types;
- raw pointers (
__ptr<T>, andvoid*) to thread-safe types; sharedfunction types, such asshared () => void(see Function types), and C function pointer types (extern (int) => int);- tuples whose elements are all thread-safe;
- inside a generic, a type parameter constrained
T: Shared.
Nothing else is. A struct is never thread-safe on its own, even when all its fields are numbers: a struct value is a reference to a reference-counted object, and only a
[Shared]struct counts those references atomically. SoString,Arrayand other structs that are not[Shared]are not thread-safe, and neither is a plain function type such as() => void:main.pep:3:14: error: field 'F' of shared struct 'H' has type '() => void', which is not thread-safe 3 | const F: () => void; | ^~~~~~~~~~ note: declare the function type 'shared () => void' to share it between threads - every number type (
A generic [Shared] struct whose parameter has no constraint is checked
for each set of type arguments, and the error points at the code that asked
for the specialization:
[Shared]
struct Cell<T> {
const value: T;
}
make<T>(): Cell<T> { return Cell<T>(); }
main(): int {
var c = make<String>();
return 0;
}
main.pep:9:13: error: 'make<String>' requires thread-safe type arguments; 'String' is not thread-safe
9 | var c = make<String>();
| ^~~~~~~~~~~~
main.pep:6:29: note: 'Cell<String>' is required here, in 'make<String>'
6 | make<T>(): Cell<T> { return Cell<T>(); }
| ^~~~~~~
main.pep:3:18: note: required by field 'value' declared here
3 | const value: T;
| ^
runtime/string.pep:54:15: note: 'String' is not declared [Shared]
A generic can state the rule up front with the built-in constraint
T: Shared: [Shared] struct Cell<T: Shared> accepts exactly the type arguments above, and a wrong one
is reported where it is written ('String' does not conform to 'Shared', required by 'T' in 'Cell'). The standard library’s Atomic<T>, spawn,
run_blocking and Lazy<T> do this, so Atomic<String> is reported at
String.
A field that is not const, or that has a type that is not thread-safe, is
reported at the field:
main.pep:3:9: error: field 'Count' of shared struct 'Counter' must be 'const'
3 | int Count;
| ^~~~~
note: threads change a shared struct's state through a thread-safe member such as 'async.Atomic<T>' (after 'import async')
Thread, ThreadLocal<T> and Atomic<T> are [Shared] themselves, and so
are the synchronization types below: Channel<T>, Mutex<T>,
Condition<T>, Once and Lazy<T>.
Module-scope variables
Every module-scope variable is implicitly shared, exported or not: any
function can use it, and any function can run on any thread. So it follows
the same two rules as a field of a [Shared] struct:
- It must be
const. To change the value, give it a thread-safe type such asAtomic<T>, or aThreadLocal<T>for a value per thread. The one exception is a[ThreadLocal]variable, which every thread has its own copy of. - It must have a thread-safe type from the list above. A
String, anArray, a struct that is not[Shared], a trait value, or a plain function value cannot be a module-scope variable; keep it in a local, or in aThreadLocal<T>.
const MAX_WORKERS = 8; # a number
const jobs_done = async.Atomic<long>(0); # changed with fetch_add
const settings = Settings(8080, 3); # a [Shared] struct
const scratch = ThreadLocal<String>(); # a String per thread
const double: shared (int) => int = (x: int) => x * 2; # a shared function
[ThreadLocal]
var depth: int = 0; # a copy per thread
A variable that breaks a rule is reported at its name or type:
main.pep:1:5: error: module-scope variable 'hits' must be 'const'
1 | var hits = 0;
| ^~~~
note: every thread can reach a module-scope variable; change its state through a thread-safe type such as 'async.Atomic<T>' (after 'import async'), or declare it '[ThreadLocal]' to give each thread its own copy
main.pep:2:15: error: module-scope variable 'origin' has type 'Plain', which is not thread-safe
2 | const origin: Plain = Plain();
| ^~~~~
main.pep:1:8: note: 'Plain' is not declared [Shared]
1 | struct Plain { var x: int; }
| ^~~~~
Module-scope variables are initialized before main runs, in the order they
are declared, and each file’s after those of the files it imports and of the
runtime. So an initializer may use the variables of the files it imports.
Channels
A Channel<T> carries values from threads that send to threads that
receive, with any number of each, like a Go channel. (In a
task, a wait parks only the
task, and so do the waits of Mutex and Condition below.)
import async;
# The main thread sends numbers to a pool of workers, and the workers send
# their squares back on a second channel.
const jobs = async.Channel<long>(16); # bounded: send waits while 16 are queued
const results = async.Channel<long>(); # unbounded: send never waits
square_numbers() {
# The loop ends once `jobs` is closed and empty.
for (var n in jobs) {
results.send(n * n);
}
}
# A `shared` function value is thread-safe, so a channel can carry work.
const tasks = async.Channel<async.ThreadBody>();
const ran = async.Atomic<long>(0);
run_tasks() {
for (var task in tasks) {
task();
}
}
main(): int {
var workers = Array<async.Thread>();
var i = 0;
while (i < 4) {
workers.append(async.Thread.spawn(square_numbers));
i = i + 1;
}
var n: long = 1;
while (n <= 100) {
jobs.send(n);
n = n + 1;
}
jobs.close(); # no more work: each worker's loop ends
for (var worker in workers) {
worker.join();
}
results.close();
var total: long = 0;
for (var square in results) {
total = total + square;
}
assert(total == 338350, "the sum of the first 100 squares");
# receive() returns a Result: an error once the channel is closed and
# empty. A closed channel refuses new values.
assert(results.receive().is_err(), "nothing left");
assert(!results.send(1), "closed");
var runner = async.Thread.spawn(run_tasks);
tasks.send(() => { ran.fetch_add(1); });
tasks.send(() => { ran.fetch_add(10); });
tasks.close();
runner.join();
assert(ran.load() == 11, "the runner ran both tasks");
return 0;
}
Channel<T>(capacity)makes a channel that holds at mostcapacityvalues;sendwaits while it is full.Channel<T>()(capacity 0) is unbounded, andsendnever waits.send(value)queues a value and returnstrue, or returnsfalseif the channel is closed.try_send(value)does not wait: it also returnsfalsewhen a bounded channel is full.receive()waits for the next value and returns aResult<T>: the value, or an error once the channel is closed and empty.try_receive()returns an error right away when nothing is waiting, andreceive_for(milliseconds)when the time runs out.close()ends the stream and wakes every waiting sender and receiver. Values already queued are still received. Onlyclose()closes a channel: dropping it, or ending the threads that use it, does not.for (var v in channel)receives until the channel is closed and empty. Several threads can loop over one channel; each value goes to one of them.- Values from one sender arrive in the order they were sent.
- The
countproperty andis_closed()report the channel’s state, which other threads may change right after.
send and try_send take a new value, which nothing on the sending
thread references, and hand it to the receiver without copying it: a
constructor call, a literal, a call that returns a fresh value,
move(x) (a variable that
holds the only reference) or copy(x)
(when the sender keeps using x). A String, an Array or a struct of them
can be sent, and changing the original after sending a copy does not change
what was sent:
import async;
# A plain struct: no destructor and no raw pointer, so `copy` copies it field
# by field. Its Strings and Array are not thread-safe, but a Channel takes
# only new values, and a copy is one.
struct Order {
String customer;
Array<String> items;
Order next;
construct(this.customer = "") {
items = Array<String>();
}
}
const orders = async.Channel<Order>();
const letters = async.Atomic<long>(0);
count_letters() {
for (var order in orders) {
for (var item in order.items) {
letters.fetch_add(item.length as long);
}
}
}
main(): int {
# copy() makes a new object for everything that is not thread-safe.
var tea = Order("ada");
tea.items.append("tea");
var copied = copy(tea);
copied.items[0].append(" and cake");
assert(tea.items[0] == "tea", "the original is unchanged");
# Objects referenced twice stay shared in the copy, and cycles are kept.
var a = Order("a");
var b = Order("b");
a.next = b;
b.next = a;
var pair = copy(a);
assert(pair.next.next === pair, "the copy has the same cycle");
a.next = __null;
pair.next = __null;
# send() takes a new value: send a copy to keep changing the order.
var worker = async.Thread.spawn(count_letters);
var order = Order("grace");
order.items.append("coffee");
orders.send(copy(order));
order.items.append("biscuits");
# Or hand the value itself over with `move`; `last` is null afterwards.
var last = Order("hopper");
last.items.append("tea");
orders.send(move(last));
orders.close();
worker.join();
assert(letters.load() == 9, "the worker saw only what was sent");
return 0;
}
Sending a variable that something else may still reference is an error, and the editor offers both fixes:
main.pep:7:16: error: 'line' is not a new value, but parameter 'value' takes a value that nothing else references
7 | lines.send(line);
| ^~~~
| move(line)
runtime/async/channel.pep:173:17: note: 'value' is declared [Move] here
173 | send([Move] value: T): bool {
| ^~~~~
note: hand it over with 'move(line)', or pass a new value, such as 'copy(line)'
A thread-safe value (a number, a [Shared] struct, a shared function value
such as a ThreadBody) passes freely, which makes a channel of ThreadBody
values a work queue. Since nothing is copied, a channel can carry any type,
trait values and values that cannot be copied (an io.File) included.
The Channel reference documents every method.
Waiting on several channels with Select
A Select<E> waits on several channels at once and takes the first value
any of them receives (or, with a send arm, sends to the first with room),
like Go’s select. Each channel is added with a
function that turns its values into an E, usually a case of an
enum of your own that carries the value, so the channels may
carry different types and a match handles the result:
import async;
# Every value the select can return: one case per channel.
enum Event {
Line(text: String),
Reading(value: long),
Stop,
}
const lines = async.Channel<String>();
const readings = async.Channel<long>();
const stop = async.Channel<bool>();
read_lines() {
lines.send("first");
lines.send("second");
lines.close();
}
read_sensor() {
var i: long = 1;
while (i <= 3) {
readings.send(i * 10);
i = i + 1;
}
readings.close();
}
main(): int {
var select = async.Select<Event>();
select.receive(lines, (text: String): Event => Event.Line(text));
select.receive(readings, (value: long): Event => Event.Reading(value));
select.receive(stop, (unused: bool): Event => Event.Stop);
var threads = [async.Thread.spawn(read_lines), async.Thread.spawn(read_sensor)];
var text = 0;
var total: long = 0;
var running = true;
while (running) {
match (select.wait()) {
Ok(event) => {
match (event) {
Line(line) => {
text = text + 1;
}
Reading(value) => {
total = total + value;
}
Stop => {
running = false;
}
}
}
Err(error) => {
# Every channel is closed and empty (`stop` included, below).
running = false;
}
}
if (text == 2 && total == 60) {
stop.close();
}
}
for (var thread in threads) {
thread.join();
}
assert(text == 2 && total == 60, "every value arrived once");
return 0;
}
receive(channel, wrap)adds a channel.wrapruns on the waiting thread, with the value received.wait()waits until a channel has a value and returns it as anE, in aResult<E>.wait_for(milliseconds)gives up after that long, andtry_wait()does not wait at all.- The value is taken under that channel’s own lock, so no other receiver can take it between “ready” and “take”.
- When several channels have values, each wait starts just after the channel that fired last, so one busy channel cannot starve the others.
- A channel that is closed and empty is skipped. Once every channel is,
wait()returns an error (every channel is closed). A timeout’s error isthe time ran out. - A
Selectbelongs to the thread (or task) that made it; it is not[Shared]. In a task, waiting parks the task, and a cancel or the task’stimeoutends the wait.
A Select can also wait to send, like a case ch <- v in Go’s select.
send(channel, value, sent) adds a send arm: wait() fires it once
channel has room (a bounded channel that is full waits), and returns what
sent returns. Here a loop keeps receiving jobs while each result waits for
room in a bounded channel:
import async;
# What one wait did: a job arrived, or a result went out.
enum Step {
Job(value: long),
Sent,
}
const jobs = async.Channel<long>();
# Holds one result at a time, so sending can wait.
const results = async.Channel<long>(1);
feed() {
var i: long = 1;
while (i <= 5) {
jobs.send(i);
i = i + 1;
}
jobs.close();
}
main(): int {
var select = async.Select<Step>();
select.receive(jobs, (value: long): Step => Step.Job(value));
var feeder = async.Thread.spawn(feed);
var received = 0;
var waiting = 0;
var total: long = 0;
while (received < 5 || waiting > 0) {
match (select.wait()) {
Ok(step) => {
match (step) {
Job(value) => {
received = received + 1;
# Goes into `results` once it has room; until then
# the select keeps receiving jobs.
select.send(results, value * value, (): Step => Step.Sent);
waiting = waiting + 1;
}
Sent => {
waiting = waiting - 1;
# Take the result back out, so the next send has room.
total = total + results.receive().value();
}
}
}
Err(error) => {
panic(error.message);
}
}
}
feeder.join();
assert(total == 55, "every square was sent once");
return 0;
}
valuemust be a new value, as forChannel.send(move(x),copy(x), a constructor call, …). The select holds it and moves it into the channel only when that arm fires, under the channel’s lock, so it is never sent when another arm fires first.- A send arm fires once: after its value is sent it leaves the select, so
the next
wait()does not send it again. Add another withsendfor the next value. - A send arm whose channel is closed is skipped like a closed receive channel, and its value is dropped with the select.
The Select reference documents every method.
Mutexes and conditions
A Mutex<T> owns a value that one thread at a time can use. The way to
reach the value is a with block:
import async;
# Each balance is an Atomic, but moving money changes both, and no thread
# may see it in neither account. A Mutex makes the pair of updates one step.
[Shared]
struct Accounts {
const checking: async.Atomic<long>;
const savings: async.Atomic<long>;
construct(long checking = 0, long savings = 0) {
this.checking = async.Atomic<long>(checking);
this.savings = async.Atomic<long>(savings);
}
}
const accounts = async.Mutex<Accounts>(Accounts(1000, 0));
const bad_totals = async.Atomic<long>(0);
move_to_savings() {
var i = 0;
while (i < 1000) {
with (var a = accounts.lock()) {
a.checking.fetch_sub(1);
a.savings.fetch_add(1);
} # unlocked here, on every path out of the block
i = i + 1;
}
}
audit() {
var i = 0;
while (i < 1000) {
with (var a = accounts.lock()) {
if (a.checking.load() + a.savings.load() != 1000) {
bad_totals.fetch_add(1);
}
}
i = i + 1;
}
}
main(): int {
var mover = async.Thread.spawn(move_to_savings);
var auditor = async.Thread.spawn(audit);
mover.join();
auditor.join();
assert(bad_totals.load() == 0, "the auditor never saw money missing");
# try_lock does not wait: the body runs if the lock was free, and the
# else block runs if another thread held it.
with (var a = accounts.try_lock()) {
assert(a.savings.load() == 1000, "everything was moved");
} else {
assert(false, "no other thread holds the lock now");
}
return 0;
}
with (var v = mutex.lock()) { ... }waits until no other thread holds the lock, binds the value, and unlocks when the block ends by any path: the end of the body,return,breakorcontinue. Everything a thread did while it held the lock is visible to the next thread that takes it.try_lock()does not wait. Withwith (var v = mutex.try_lock()) { ... } else { ... }, the body runs if the lock was free and theelseblock runs if it was not.- A Mutex is not re-entrant: calling
lock()again on the thread that holds it panics instead of deadlocking.is_held()reports whether the calling thread holds it. - A
panicwhile a Mutex is held ends the program, so a Mutex is never left “poisoned”. - Taking a free lock is one atomic instruction, and a thread that finds it
held spins briefly before it sleeps. Locks sleep on the operating system’s
wait-on-address call (
futexon Linux,__ulock_waiton macOS,WaitOnAddresson Windows), as Rust’s do.
T is a struct type: a struct of your own, an Array<String>, or a
[Shared] struct whose Atomic fields must change as a unit.
Mutex<int> is an error (use async.Atomic<int>). A Mutex of values that are not
thread-safe is kept apart from the rest of the program by the compiler; see
What a lock body may do.
Mutex<T>(value)andreplace(value)take a new value: a constructor call, a string literal, a call that returns a fresh value,copy(x), ormove(x), never a variable something else still references. The caller hands its reference over, so nothing is copied.replace(value)swaps the whole value under the lock and returns the old one, which the caller then owns outright.
A value built in a local variable goes in with
move, which hands over the
variable’s reference instead of copying the value:
import async;
main(): int {
# Build the list here, then hand it to the Mutex: `move` gives up
# `lines`' reference, so nothing is copied and `lines` is empty after.
var lines = Array<String>();
lines.append("first");
var log = async.Mutex<Array<String>>(move(lines));
var line = "second";
with (var list = log.lock()) {
# `line` holds the only reference to its String, so it can move in.
list.append(move(line));
}
# Swap in a new list; the old one comes back without a copy either.
var next = Array<String>();
next.append("third");
var old = log.replace(move(next));
print($"{old.count}"); # 2
print(old[1]); # second
return 0;
}
A Condition<T> lets a thread that holds a Mutex sleep until another thread
changes the value. Get one with mutex.condition():
import async;
# A baker makes pies and eaters take them. The eaters sleep on a Condition
# until there is a pie, or until the bakery closes.
[Shared]
struct Bakery {
const pies: async.Atomic<long>;
const closed: async.Atomic<int>;
construct() {
pies = async.Atomic<long>(0);
closed = async.Atomic<int>(0);
}
}
const bakery = async.Mutex<Bakery>(Bakery());
const changed = bakery.condition();
const eaten = async.Atomic<long>(0);
eat() {
while (true) {
with (var b = bakery.lock()) {
# Wait in a loop: a wakeup can be spurious, and another eater
# may take the pie first.
while (b.pies.load() == 0 && b.closed.load() == 0) {
changed.wait(); # unlocks, sleeps, locks again
}
if (b.pies.load() == 0) {
return; # closed and nothing left; the block unlocks
}
b.pies.fetch_sub(1);
}
eaten.fetch_add(1);
}
}
main(): int {
var eaters = Array<async.Thread>();
var i = 0;
while (i < 3) {
eaters.append(async.Thread.spawn(eat));
i = i + 1;
}
i = 0;
while (i < 50) {
with (var b = bakery.lock()) {
b.pies.fetch_add(1);
changed.notify_one();
}
i = i + 1;
}
with (var b = bakery.lock()) {
b.closed.store(1);
changed.notify_all();
}
for (var eater in eaters) {
eater.join();
}
assert(eaten.load() == 50, "every pie was eaten once");
return 0;
}
wait()must run while the caller holds the Mutex, normally inside itswithblock. It unlocks, sleeps until notified, and locks again before returning. It panics if the caller does not hold the Mutex.- A thread can wake without a notification (a spurious wakeup), and another thread may get to the value first, so always wait in a loop that re-checks what it waits for.
wait_for(milliseconds)gives up after that long and returnsfalse.notify_one()wakes one waiting thread, andnotify_all()wakes them all.
wait_while(predicate) writes that loop for you: it waits while
predicate returns true for the locked value, asking again after every
wakeup, and returns with the Mutex still locked. wait_while_for(milliseconds, predicate) gives up after that long and returns false:
import async;
# A worker takes jobs from a queue, sleeping while it is empty.
const jobs = async.Mutex<Array<long>>(Array<long>());
const changed = jobs.condition();
const done = async.Atomic<long>(0);
work() {
var taken = 0;
while (taken < 5) {
with (var queue = jobs.lock()) {
# Waits while the predicate holds; spurious wakeups are handled.
# The predicate is given the locked value: it cannot capture it.
changed.wait_while((waiting: Array<long>): bool => waiting.count == 0);
done.fetch_add(queue.remove_at(0));
taken = taken + 1;
}
}
}
main(): int {
var worker = async.Thread.spawn(work);
var i: long = 1;
while (i <= 5) {
with (var queue = jobs.lock()) {
queue.append(i);
changed.notify_all();
}
i = i + 1;
}
worker.join();
assert(done.load() == 15, "every job was taken once");
with (var queue = jobs.lock()) {
# Gives up after 10 milliseconds: nothing is queued.
var ready = changed.wait_while_for(10, (waiting: Array<long>): bool => waiting.count == 0);
assert(!ready, "the time ran out");
}
return 0;
}
The predicate is given the locked value as its parameter, since a closure cannot capture part of a locked value. It must be a closure written at the call, and the compiler checks its body as it checks a lock body (see What a lock body may do): it cannot keep any part of the value, or bring in a value from outside.
main.pep:10:26: error: 'queue[0]' is part of the value locked by 'queue' and cannot outlive the lock
10 | saved.append(queue[0]);
| ^~~~~~~~
main.pep:9:27: note: locked here
9 | ready.wait_while((queue: Array<String>): bool => {
| ^~~~~
note: 'String' is not thread-safe, so no part of the locked value may be kept outside the block; take 'copy(queue[0])'
The Mutex reference documents Mutex,
MutexGuard and Condition.
What a lock body may do
A value that is not thread-safe counts its references plainly, so no thread
may hold a reference into a Mutex’s value while the Mutex is unlocked. The
compiler checks each with block over a lock so that none can:
import async;
# A Mutex of values that are not thread-safe: a list of Strings that
# several threads add to.
const names = async.Mutex<Array<String>>(Array<String>());
add_names(prefix: String) {
var i = 0;
while (i < 3) {
with (var list = names.lock()) {
list.append("worker"); # a new String: fine
list.append(prefix + "!"); # `+` returns a new String: fine
list.append(copy(prefix)); # a copy of an outside value: fine
var n = list.count; # a number leaves freely
if (list.contains(prefix)) { # comparing keeps nothing
n = n + 1;
}
}
i = i + 1;
}
}
# Part of the locked value leaves the block only as a copy.
first_name(): String {
with (var list = names.lock()) {
if (list.count > 0) {
return list[0].copy();
}
}
return "";
}
main(): int {
var a = async.Thread.spawn(() => { add_names("a"); });
var b = async.Thread.spawn(() => { add_names("b"); });
a.join();
b.join();
var first = first_name();
first.append(" (mine)"); # changing the copy leaves the list alone
with (var list = names.lock()) {
assert(list.count == 18, "every name was added");
assert(list[0] == "worker", "the locked String is unchanged");
}
# replace swaps in a new value and hands back the old one, which the
# caller then owns outright.
var old = names.replace(Array<String>());
assert(old.count == 18, "the old list");
# A thread's own value that is not thread-safe is reached through `with`.
var scratch = async.ThreadLocal<String>();
scratch.set("draft");
with (var text = scratch.enter()) {
text.append(" 2");
}
with (var text = scratch.enter()) {
assert(text == "draft 2", "this thread's value");
}
return 0;
}
Inside the block, every value that is not thread-safe is either part of
the locked value (the binding, its fields and elements, and what calls on
them return), from outside (a parameter, this, or a variable declared
before the block), or new. A new value may join either side; the other
two must never meet:
-
Moving in. A call, constructor or assignment that would link an outside value with the locked one is an error. Pass a new value instead:
copy(line), a literal, or the result of a call that returns a fresh value. A variable that holds the only reference to its value can hand it over withmove(line).main.pep:8:21: error: 'line' cannot move into the value locked by 'names' 8 | list.append(line); | ^~~~ main.pep:7:28: note: locked here 7 | with (var list = names.lock()) { | ^~~~ note: 'String' is not thread-safe, so the locked value cannot share it with the rest of the program; pass 'copy(line)' -
Moving out. Storing part of the locked value outside the block, or returning it from the block, is an error; take a copy (
copy(list[0]), orlist[0].copy(), which returns a fresh value).main.pep:8:17: error: 'list[0]' is part of the value locked by 'names' and cannot outlive the lock 8 | first = list[0]; | ^~~~~~~ -
Closures. A closure written in the block cannot capture part of the locked value, since a function value could carry it out.
-
Two locks. Values locked by two nested blocks stay apart as well.
-
Numbers pass freely, as do every thread-safe value and every new one.
Which calls link which values comes from what the called function does: the
compiler works out, for every function, whether it keeps an argument (as
append keeps its element) and whether its result shares anything with its
arguments. A result that shares nothing, such as that of +, substr,
copy() or a constructor, is fresh, and may cross in either direction.
The language server shows “Returns a fresh value” when hovering over such a
function. A call through a function value, a value an extern function
hands back, and a value read through raw memory that a [Shared] struct
holds are never fresh: something else may reference them. The runtime takes
the few values it hands out that nothing else references out of raw memory
with move(p[i]), a promise the compiler cannot check and allows only in the
runtime: a channel’s receive() (the value send handed over) and a Mutex’s
replace().
A function that returns a tuple has one such fact per element:
after var (fresh, same) = split(line);, fresh may move into a locked value
while same, which split returned from its argument, may not.
The guard a lock() returns can only be the scope of a with header: storing
it in a variable or field is an error, so no lock outlives its block.
Two attributes carry these rules, and code outside the runtime may use them too:
[Isolated]on a struct makes it a guard: it can only be the scope of awithheader, and the block is checked as above.MutexGuard<T>andThreadLocalGuard<T>are[Isolated].[Move]on a parameter (construct([Move] T value)) requires a new value for it at every call: a constructor call, a literal, a fresh result,copy(x),move(x), or any thread-safe value. The caller hands its reference over, so the function receives it without a count step.
Checking confinement at run time
Raw memory (__ptr<T>) and extern code are promises the compiler cannot
check, so a program that uses them can still keep a reference into a locked
value. Set the environment variable PEP_CHECK_SYNC when running a program
to check at every unlock that nothing outside the locked value refers into
it:
$ PEP_CHECK_SYNC=1 ./main
PEP_CHECK_SYNC: an object in the value locked by a 'Mutex<Box>' is still referenced from outside it
The check copies the locked value to count the references inside it, so it
costs time and memory in proportion to the value’s size at each unlock; it
is off unless the variable is set. It checks a Mutex<T> whose T is not
thread-safe and can be copied.
Reader/writer locks
An RwLock<T> lets many threads read a value at once, or one thread
write it:
import async;
# A price list that many threads read and one thread now and then updates.
# A writer changes both prices together, so a reader never sees one updated
# and the other not.
[Shared]
struct Prices {
const coffee: async.Atomic<long>;
const tea: async.Atomic<long>;
construct() {
coffee = async.Atomic<long>(300);
tea = async.Atomic<long>(250);
}
}
const prices = async.RwLock<Prices>(Prices());
const mismatches = async.Atomic<long>(0);
check_prices() {
var i = 0;
while (i < 1000) {
with (var p = prices.read()) { # many readers at once
if (p.coffee.load() - p.tea.load() != 50) {
mismatches.fetch_add(1);
}
}
i = i + 1;
}
}
raise_prices() {
var i = 0;
while (i < 100) {
with (var p = prices.write()) { # no reader sees the middle
p.coffee.fetch_add(10);
p.tea.fetch_add(10);
}
i = i + 1;
}
}
main(): int {
var threads = Array<async.Thread>();
var i = 0;
while (i < 3) {
threads.append(async.Thread.spawn(check_prices));
i = i + 1;
}
threads.append(async.Thread.spawn(raise_prices));
for (var thread in threads) {
thread.join();
}
assert(mismatches.load() == 0, "no reader saw a half-done update");
with (var p = prices.read()) {
assert(p.coffee.load() == 1300, "every raise landed");
}
return 0;
}
with (var v = lock.read()) { ... }waits until no writer holds or waits for the lock; any number of threads may read at the same time.with (var v = lock.write()) { ... }waits until no other thread holds the lock at all.try_read()andtry_write()do not wait: theelseblock runs when the lock is not free.replace(value)swaps the whole value under the write lock and returns the old one.- Writers come first: while a writer waits, new readers wait behind it, so readers cannot keep it out forever. A thread that reads must therefore not ask to read again before its block ends, and asking to write while holding the lock panics instead of deadlocking.
T must be thread-safe, as for Atomic<T>: typically a [Shared] struct
whose Atomic fields a writer changes together. Readers that hold the
value at once each copy a reference to it, so a value that is not
thread-safe, such as an Array<String>, would have its reference counts
changed by several threads at a time; it is a compile error at the type
argument. Put such a value in a Mutex<T>. When readers only need the
latest value and writers replace it whole, an Atomic<T> holding a
[Shared] struct does the same job without a lock.
Semaphores, barriers and latches
Three types coordinate threads by counting. Each is [Shared], so keep it
in a module-scope variable or pass it to threads:
- A
Semaphore(permits)lets at mostpermitsthreads hold a permit at once.acquire()waits for a free permit and takes it,try_acquire()takes one only if one is free, andrelease()gives one back.with (semaphore.permit()) { ... }holds a permit for the block and gives it back on every path out. - A
Barrier(count)makes groups ofcountthreads meet:wait()sleeps untilcountthreads have called it, then all go on together. It returnstrueon exactly one of them, the leader, and resets for the next round. - A
Latch(count)opens oncecount_down()has been calledcounttimes.wait()sleeps until it is open, andis_ready()asks without waiting. It is one-shot: counting down an open latch panics.
import async;
const WORKERS = 4;
# At most two workers use the printer at once.
const printer = async.Semaphore(2);
const printing = async.Atomic<long>(0);
const most_printing = async.Atomic<long>(0);
# Every worker finishes a step before any starts the next.
const step_done = async.Barrier(WORKERS);
const steps_led = async.Atomic<long>(0);
# The main thread waits until every worker has set up.
const ready = async.Latch(WORKERS);
work() {
ready.count_down();
var step = 0;
while (step < 3) {
with (printer.permit()) {
var now = printing.fetch_add(1) + 1;
if (now > most_printing.load()) {
most_printing.store(now);
}
printing.fetch_sub(1);
} # the permit is given back here
if (step_done.wait()) {
steps_led.fetch_add(1); # one leader per step
}
step = step + 1;
}
}
main(): int {
var workers = Array<async.Thread>();
var i = 0;
while (i < WORKERS) {
workers.append(async.Thread.spawn(work));
i = i + 1;
}
ready.wait();
assert(ready.is_ready(), "every worker counted down");
for (var worker in workers) {
worker.join();
}
assert(most_printing.load() <= 2, "the semaphore let two in at most");
assert(steps_led.load() == 3, "each step had exactly one leader");
return 0;
}
Each is a Mutex over its counts with a Condition to sleep on, so in a
task their waits park the task,
not its worker.
Once and Lazy
Once runs a function exactly once, however many threads ask, and Lazy<T>
builds a value the first time a thread asks for it:
import async;
[Shared]
struct Settings {
const workers: long;
construct(this.workers = 1) {}
}
const builds = async.Atomic<long>(0);
load_settings(): Settings {
builds.fetch_add(1);
return Settings(async.logical_cpus() as long);
}
# Built by the first thread that calls get(), not before main runs.
const settings = async.Lazy<Settings>(load_settings);
# Runs initialize() once, however many threads call it.
const setup = async.Once();
const setups = async.Atomic<long>(0);
initialize() {
setups.fetch_add(1);
}
work() {
setup.call(initialize);
assert(settings.get().workers >= 1, "every thread sees the settings");
}
main(): int {
assert(!settings.is_built(), "nothing is built before first use");
var workers = Array<async.Thread>();
var i = 0;
while (i < 4) {
workers.append(async.Thread.spawn(work));
i = i + 1;
}
for (var worker in workers) {
worker.join();
}
assert(builds.load() == 1, "the settings were built once");
assert(setups.load() == 1, "initialize ran once");
assert(setup.is_done(), "done");
return 0;
}
once.call(body)runsbodyif no call has run one yet. Threads that call it while the body runs wait for it, and every caller sees what the body did.is_done()reports whether it has run.Lazy<T>(initializer)takes ashared () => Tfunction. The firstget()runs it; later calls, from any thread, return the same value.is_built()reports whether it has run.- A
Lazy<T>is the usual way to give a module-scope variable a value that is expensive to build, or that cannot be built beforemainruns. ItsTmust be thread-safe, since every thread gets the same value.
The Once reference has the details.
Limitations
Lazy<T>holds only thread-safe values.- A value received from a channel moves into a locked value directly
(
list.append(jobs.receive().value())), but not through a variable declared outside the block: such a variable is an outside value, whatever it holds, so it needscopy, ormovewhen it holds the only reference (list.append(move(job))). - There is no reader-writer lock, and no barrier or semaphore. A Mutex is not re-entrant.
- The value a
Select’swait()returns is not a new value for a lock body, sincewrapis a function value the compiler cannot follow;copyit to move it into a locked value. - Deadlocks are not detected, except locking a Mutex twice on one thread, which panics.
Tasks
A task is a function that runs alongside others. Each task has its own small
stack, and the tasks on a thread take turns: one runs until it waits (for a
timer, for another task, or by yielding), and then another runs. A runtime
has a few such threads, its workers, so tasks also run in parallel. This is
the model of Go’s goroutines and of libuv or tokio event loops, without
async/await: a function that waits looks like any other function, and
any function can wait, however deep in the call stack. A socket call that
has to wait parks only its task, so one worker can serve thousands of
connections.
WASI cannot switch stacks, so on WebAssembly (--target wasm32-wasip1)
starting tasks (async.run, async.run_with) panics (see WebAssembly).
Tasks live in the async module:
import async;
main(): int {
var log = Array<String>();
async.run(() => {
# A task starts when the running task next waits.
var worker = async.Task<int>.spawn_local(() => {
log.append("worker starts");
async.yield_now();
log.append("worker ends");
return 42;
});
log.append("first task");
var answer = worker.join().value(); # waits; the worker runs
assert(answer == 42, "join returns the body's value");
log.append("joined");
});
assert(log[0] == "first task" && log[1] == "worker starts", "tasks take turns");
assert(log[2] == "worker ends" && log[3] == "joined", "join waited");
return 0;
}
async.run(body)runsbodyas the first task and returns once it has returned and every task it started has finished.run_with(options, body)does the same with settings;RuntimeOptions().stack_size(bytes)sets how much stack each task reserves (256 KiB by default).Task<T>.spawn_local(body)starts a task on the running task’s worker and returns itsTask<T>. The new task runs the next time the running task waits.Tis the body’s result type; useTask<void>for a body that returns nothing.task.join()waits for the task and returns aResult<T, TaskError>: the body’s value, or an error if the task was cancelled.async.yield_now()lets every other ready task run first.task.is_finished()says whether the body has returned.
A task from spawn_local stays on the thread of the task that started it,
so it may use any value that task uses, as the closures above share log.
Nothing needs to be thread-safe.
Several workers
run turns the calling thread into the first worker and starts one more
thread per logical CPU after it (RuntimeOptions().workers(count) sets how
many workers in all). async.spawn<T>(body) starts a task that whichever
worker is free first runs, in parallel with the others:
import async;
sum(from: long, to: long): long {
var total: long = 0;
var i = from;
while (i < to) {
total = total + i;
i = i + 1;
}
return total;
}
main(): int {
async.run_with(async.RuntimeOptions().workers(4), () => {
# Two halves of the work, on two workers at the same time.
var low = async.spawn<long>(() => { return sum(0, 500000); });
var high = async.spawn<long>(() => { return sum(500000, 1000000); });
var total = low.join().value() + high.join().value();
assert(total == 499999500000, "both halves were added");
});
return 0;
}
Because such a task may run on another thread, its body must be a shared
function: it may capture only thread-safe values, as a
thread’s body. Its result crosses threads too, so spawn
constrains it T: Shared: a
thread-safe type such as a number, a
[Shared] struct, or a shared function type. async.spawn<String> is a
compile error at String. join, cancel and
is_finished work as for spawn_local, from the task that started it.
A task, once started, stays on its worker until it finishes, so the tasks it
starts with spawn_local share values with it safely. An idle worker takes
the next task that no worker has started yet. A task that computes without
waiting still holds up the other tasks on its own worker (not the other
workers), so a long computation should call yield_now() now and then.
async.spawn_on_each_worker(body) starts one task per worker, each on its
own worker, and returns them in worker order. It is how a server runs an
accept loop on every worker (see Sockets).
Blocking calls
Some work would hold a worker up: a call that blocks in the operating system
(reading a file, a C library that waits) or a long computation.
async.run_blocking<T>(body) runs body on a separate thread and parks
the calling task until it returns, so the worker keeps running its other
tasks meanwhile:
import async;
const ticks = async.Atomic<int>(0);
main(): int {
async.run_with(async.RuntimeOptions().workers(1), () => {
# Keeps running on the one worker while the blocking call waits.
var ticker = async.Task<void>.spawn_local(() => {
while (ticks.load() < 3) {
async.sleep(5);
ticks.fetch_add(1);
}
});
var answer = async.run_blocking<int>(() => {
async.Thread.sleep(50); # blocks a pool thread, not the worker
return 42;
});
assert(answer == 42, "the blocking call's result");
assert(ticks.load() > 0, "the worker ran the ticker meanwhile");
ticker.join();
});
return 0;
}
The runtime keeps a pool of threads for these calls. It starts one when a
call finds none idle, up to RuntimeOptions().blocking_threads(count) (64
by default); past that, calls wait for a free one. As for spawn, body
must be a shared function and T must be thread-safe. body always runs
to the end: cancelling the task, or a timeout around the call, takes
effect once the call has returned. Outside a task, run_blocking just
calls body.
Waiting
async.sleep puts a task to sleep and runs the others meanwhile, so many
tasks can wait at the same time on one thread. It takes an os.Duration
(see The operating system), or a number of milliseconds; a duration
that is not a whole number of milliseconds rounds up:
import async;
import os;
main(): int {
var order = Array<int>();
async.run(() => {
var tasks = Array<async.Task<void>>();
var i = 0;
while (i < 3) {
var n = i;
# The later a task starts, the shorter it sleeps.
tasks.append(async.Task<void>.spawn_local(() => {
async.sleep(os.Duration.from_milliseconds(((3 - n) * 20) as ulong));
order.append(n);
}));
i = i + 1;
}
for (var task in tasks) {
task.join();
}
});
# All three slept at the same time, on one thread.
assert(order[0] == 2 && order[1] == 1 && order[2] == 0, "shortest sleep first");
return 0;
}
Outside a task, sleep blocks the thread, and yield_now does nothing.
Tasks are never interrupted. A task that computes for a long time without
waiting keeps every other task waiting, so a long loop should call
yield_now() now and then.
Thread.sleep in a task is async.sleep.
Mutexes, channels and conditions
The synchronization types work in tasks too. When a task
has to wait for a Mutex, for a value from a Channel or for room in a
bounded one, in Condition.wait, for an RwLock, or in Semaphore.acquire,
Barrier.wait or Latch.wait, it parks and its worker runs other
tasks, just as it does for a socket. The task that holds the lock or will
send the value can be on the same worker, on another worker, or a plain
thread:
import async;
struct Log {
var lines: Array<String>;
construct() {
this.lines = Array<String>();
}
}
const log = async.Mutex<Log>(Log());
const jobs = async.Channel<int>(2);
main(): int {
# One worker: every task below shares one thread.
async.run_with(async.RuntimeOptions().workers(1), () => {
# The consumer parks in the channel until a value arrives.
var consumer = async.Task<int>.spawn_local(() => {
var total = 0;
for (var job in jobs) {
total = total + job;
}
return total;
});
# A task may hold the lock while it waits; the other task parks
# until it is free.
var writer = async.Task<void>.spawn_local(() => {
with (var l = log.lock()) {
l.lines.append("first");
async.sleep(10);
l.lines.append("second");
}
});
async.yield_now();
with (var l = log.lock()) {
l.lines.append("third");
}
writer.join();
# The producer parks while the bounded channel is full.
var i = 1;
while (i <= 5) {
jobs.send(i);
i = i + 1;
}
jobs.close();
assert(consumer.join().value() == 15, "every job arrived");
});
with (var l = log.lock()) {
assert(l.lines[2] == "third", "the lock waited for the writer");
}
return 0;
}
- Each task counts as its own holder of a
Mutex, so two tasks on one worker exclude each other, and a task may hold a lock across its own waits (asleep, a socket call, another lock). - A cancel, or a
timeoutaround the call, ends a channel wait:sendreturns false andreceivereturns an error (and aforloop over the channel ends). A task waiting for aMutexstill gets the lock, andCondition.waitstill waits for a notification, since the caller’s loop would only wait again;wait_forreturns when its own time runs out. - A
Selectparks its task on every channel at once, and a cancel ortimeoutends its wait with an error.Condition.wait_whileparks likewait. - Outside a task they block the thread, as before.
Task-local values
A TaskLocal<T> is a variable with a separate value in every task, as a
ThreadLocal<T> has one
per thread. Tasks that take turns on one worker share its thread, so only a
TaskLocal can tell them apart:
import async;
# The id of the request the running task is handling.
const request_id = async.TaskLocal<long>();
# Deep in the call chain, without the id passed down as a parameter.
current_request(): long {
return request_id.get();
}
main(): int {
# One worker: the three tasks take turns on one thread, so a
# ThreadLocal could not tell them apart.
async.run_with(
async.RuntimeOptions().workers(1),
() => {
var tasks = Array<async.Task<bool>>();
for (var id in [1, 2, 3]) {
tasks.append(
async.Task<bool>.spawn_local(
() => {
request_id.set(id);
async.yield_now(); # the other tasks set their ids meanwhile
return current_request() == id;
}
)
);
}
for (var task in tasks) {
assert(task.join().value(), "each task sees its own id");
}
# This task never set one.
assert(!request_id.is_set(), "a task starts with no value");
}
);
return 0;
}
set(value)stores the calling task’s value andget()returns it (the default ofT, zero or null, until the task sets one).is_set()andclear()work as on aThreadLocal.- A task starts with no value, even one started by a task that has one, and its values are released when it finishes.
- Code outside any task (before
async.run, or on a plainThread) has one value per thread, like aThreadLocal. - For a
Tthat is not thread-safe, such as aString, reach the value withwith (var value = local.enter()) { ... }, as for aThreadLocal;get()is then a compile error.
Sockets
A socket call in a task that would have to wait (for a connection, for data, or for room to send) parks the task and lets the others run until the socket is ready. The socket API is the same one a plain program uses; only the waiting differs:
import async;
import io;
# Echoes everything a client sends until it closes the connection.
serve(stream: io.TcpStream) {
var buffer = Array<u8>();
while (true) {
var r = stream.read(buffer, 4096); # parks this task until data arrives
if (r.is_err() || r.value() == 0) {
return;
}
stream.write_all(buffer);
buffer.clear();
}
}
main(): int {
async.run(() => {
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var address = listener.local_address().value();
var clients = 10;
# The server: one task per connection, all on this thread.
async.Task<void>.spawn_local(() => {
while (true) {
var accepted = listener.accept();
if (accepted.is_err()) {
return; # cancelled when the first task returns
}
var (stream, peer) = accepted.value();
async.Task<void>.spawn_local(() => { serve(stream); });
}
});
# The clients, as tasks on the same thread.
var replies = Array<async.Task<ulong>>();
var i = 0;
while (i < clients) {
var k = i;
replies.append(async.Task<ulong>.spawn_local(() => {
var stream = io.TcpStream.connect(address).value();
var message = $"hello from {k}";
stream.write_all(message);
var buffer = Array<u8>();
stream.read_exact(buffer, message.size);
return buffer.count;
}));
i = i + 1;
}
for (var reply in replies) {
assert(reply.join().value() > 0, "the server echoed the message");
}
});
return 0;
}
Outside a task, the same calls block the thread as before. A socket belongs
to the worker whose task made it: tasks started with spawn_local can share
it, and each spawned task makes its own.
Waiting on several sockets
To wait on whichever of several sockets is ready first, give each socket a
task that reads it and sends what it read to a channel, and wait on the
channels with a Select.
Closing a channel when its socket closes lets the select stop waiting on it.
Tasks are cheap, so this costs little, and the reading code stays the
ordinary blocking-style loop:
import async;
import io;
# What the select returns: bytes that arrived on one of the connections.
enum Event {
Data(from: int, count: ulong),
}
# Reads `stream` until the peer closes it, sending the size of each read to
# `into`, then closes `into` so the select stops waiting on it.
forward(stream: io.TcpStream, into: async.Channel<ulong>) {
var buffer = Array<u8>();
while (true) {
var r = stream.read(buffer, 4096); # parks this task, not the others
if (r.is_err() || r.value() == 0) {
into.close();
return;
}
into.send(r.value());
buffer.clear();
}
}
main(): int {
async.run(() => {
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var address = listener.local_address().value();
var first = io.TcpStream.connect(address).value();
var (from_first, first_peer) = listener.accept().value();
var second = io.TcpStream.connect(address).value();
var (from_second, second_peer) = listener.accept().value();
# One task per socket turns what it reads into channel messages.
var first_bytes = async.Channel<ulong>();
var second_bytes = async.Channel<ulong>();
async.Task<void>.spawn_local(() => { forward(from_first, first_bytes); });
async.Task<void>.spawn_local(() => { forward(from_second, second_bytes); });
first.write_all("hello");
first.shutdown_write();
second.write_all("hi there");
second.shutdown_write();
# The select then waits on both sockets at once, through their channels.
var select = async.Select<Event>();
select.receive(first_bytes, (count: ulong): Event => Event.Data(0, count));
select.receive(second_bytes, (count: ulong): Event => Event.Data(1, count));
ulong first_total = 0;
ulong second_total = 0;
var running = true;
while (running) {
match (select.wait()) {
Ok(event) => {
match (event) {
Data(from, count) => {
if (from == 0) {
first_total = first_total + count;
} else {
second_total = second_total + count;
}
}
}
}
Err(error) => {
running = false; # both connections closed
}
}
}
assert(first_total == 5 && second_total == 8, "every byte arrived once");
});
return 0;
}
Files
File.read, write and write_all in a task park the task until the
system call is done, and the worker runs other tasks meanwhile. With the
IoUring backend the kernel does the read or write itself; with the others
a thread from run_blocking’s pool makes the blocking call. Opening,
seeking, metadata and the other File calls still block the thread, as
they are quick on local disks. A cancel or a timeout does not cut a read
or write short: it takes effect once the call returns.
The array a read fills or a write sends from is in use until the call
returns, so growing it from another task meanwhile panics (“Array: the
buffer is in use by an I/O operation”). A String passed to write_all is
copied first.
Accepting on every worker
An io.SharedTcpListener is a listening socket that any thread can accept
on. Give each worker an accept loop with spawn_on_each_worker; each
connection goes to one of the waiting workers, and the stream it returns
belongs to that worker:
import async;
import io;
const served = async.Atomic<int>(0);
main(): int {
async.run_with(async.RuntimeOptions().workers(4), () => {
var listener = io.SharedTcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var port = listener.local_address().value().port();
# One accept loop per worker; each connection goes to one of them.
async.spawn_on_each_worker(() => {
while (true) {
var accepted = listener.accept();
if (accepted.is_err()) {
return; # cancelled when the first task returns
}
var (stream, peer) = accepted.value();
served.fetch_add(1);
async.Task<void>.spawn_local(() => {
stream.write_all("hello").value();
});
}
});
var i = 0;
while (i < 8) {
var client = io.TcpStream.connect_host("127.0.0.1", port).value();
var reply = Array<u8>();
while (client.read(reply, 64).value() > 0) {
}
assert(reply.count == 5, "greeted");
i = i + 1;
}
});
assert(served.load() == 8, "every connection was accepted once");
return 0;
}
The listener has no close(), since other workers may be waiting on it.
It closes when the last reference goes: here, once run has cancelled the
loops.
Moving a stream to another worker
stream.into_transfer() moves a connection out of a TcpStream into an
io.SocketTransfer, a thread-safe token that a shared function can
capture or a Channel can carry, and io.TcpStream.adopt(transfer) takes
it out again on the receiving worker. The old stream is closed afterwards,
only the first adopt gets the connection, and a token no one adopts
closes the connection when it goes:
import async;
import io;
main(): int {
async.run_with(async.RuntimeOptions().workers(2), () => {
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var port = listener.local_address().value().port();
var client = async.Task<ulong>.spawn_local(() => {
var stream = io.TcpStream.connect_host("127.0.0.1", port).value();
var reply = Array<u8>();
while (stream.read(reply, 64).value() > 0) {
}
return reply.count;
});
var (stream, peer) = listener.accept().value();
# A stream is not thread-safe; its transfer token is.
var transfer = stream.into_transfer();
var handler = async.spawn<void>(() => {
var mine = io.TcpStream.adopt(transfer).value();
mine.write_all("from any worker").value();
});
handler.join();
assert(client.join().value() == 15, "the moved stream answered");
});
return 0;
}
When no task is ready to run, a worker blocks until a socket is ready, the next timer is due, or another worker wakes it; while tasks are running, it also checks the sockets every few dozen tasks, so a busy task cannot starve the network.
Backends
How the runtime waits for sockets is its I/O backend, an async.Backend:
| Backend | Where | How |
|---|---|---|
Epoll | Linux | epoll, edge-triggered |
IoUring | Linux 5.11 and later | io_uring: the kernel does stream sockets’ receives and sends and files’ reads and writes itself, submitted in batches, with deferred completion work; never picked automatically, since container seccomp profiles and hardened kernels often refuse it |
Kqueue | macOS | kqueue, edge-triggered |
Iocp | Windows | an I/O completion port, fed by AFD poll requests (as tokio and libuv do) |
Poll | everywhere | poll() (WSAPoll on Windows), which looks at every waiting socket on each wait: fine for a few connections, slow for thousands |
Backend.Automatic, the default, picks epoll, kqueue or IOCP, whichever
the system has. RuntimeOptions().backend(kind) asks for one (and
run_with panics if the system does not have it), and the
PEPPER_IO_BACKEND environment variable (epoll, io_uring, kqueue,
iocp or poll) overrides the automatic choice without changing the
program, for testing and benchmarks. async.current_backend() says which one is running.
io_uring pays off as the amount of multiplexed I/O grows: many operations
in flight, submitted and completed in batches through the ring rather than
one system call each. With the IoUring backend, a stream socket’s read
and write in a task go to the kernel as a receive or send request, which
it runs once the socket is ready; the worker submits every request queued
since its last look, and collects every completion, in one system call. In
our echo benchmark that makes about an eighth of epoll’s system calls,
runs as fast as epoll from 64 to 1,024 connections (half again as fast as
io_uring used only for readiness), and stays about 10% behind at 4,096.
A cancel or a timeout asks the kernel to stop a request and waits for it
to finish, since the kernel is writing into or reading from the array
until then. A receive that got data before the cancel took effect returns
that data instead of failing, so no bytes are lost. As with files, growing
the array from another task meanwhile panics. Accepting, connecting, UDP
sockets, and a second task reading (or writing) a socket that one already
reads (or writes) still wait for readiness.
Timeouts
async.timeout(milliseconds) limits how long the waits inside a with
block may take:
import async;
import io;
main(): int {
async.run(() => {
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
# Nobody connects, so the accept gives up after 50 milliseconds.
with (var deadline = async.timeout(50)) {
var accepted = listener.accept();
assert(accepted.is_err() && accepted.error().is_timed_out(), "accept timed out");
assert(deadline.is_expired(), "the limit has passed");
}
# A join gives up too; the task itself carries on until cancelled.
var slow = async.Task<int>.spawn_local(() => {
async.sleep(1000000);
return 42;
});
with (async.timeout(20)) {
var result = slow.join();
assert(result.error().is_timed_out(), "join timed out");
}
slow.cancel();
});
return 0;
}
Once the limit passes, a socket call that has to wait fails with an
IoError for which is_timed_out() holds, join fails with a TaskError
for which is_timed_out() holds (the task it waited for carries on), and
sleep returns early. Code that does not wait is never interrupted. The
limit ends with the block. Timeouts nest, and an inner one cannot extend an
outer one. deadline.is_expired() says whether the limit has passed.
timeout panics outside a task.
Cancelling
task.cancel() asks a task to stop. Nothing stops it by force: from then on
async.is_cancelled() is true inside it, sleep returns at once, and
join on it fails with a TaskError whose is_cancelled() is true. A
task waiting in join returns the same error when it is cancelled itself,
and a socket call waiting in a cancelled task fails with an IoError whose
is_cancelled() is true.
When the first task of a run returns, every task still running is
cancelled, on every worker, and run waits for each to finish:
import async;
main(): int {
var polls = Array<int>();
async.run(() => {
var poller = async.Task<void>.spawn_local(() => {
while (!async.is_cancelled()) {
polls.append(1);
async.sleep(5);
}
});
async.sleep(30);
poller.cancel();
var result = poller.join();
assert(result.is_err() && result.error().is_cancelled(), "a cancelled task's join fails");
# A task still running when the first task returns is cancelled too,
# and run waits for it to finish.
async.Task<void>.spawn_local(() => {
async.sleep(1000000); # returns at once when cancelled
});
});
assert(polls.count > 0, "the poller ran until cancelled");
return 0;
}
If every task is waiting for another task and none can continue, run
panics instead of waiting forever.
How it works
Each task runs on a fiber: a stack of its own and a saved set of
registers. Switching tasks saves the running task’s registers on its stack
and restores the other’s, a few instructions of assembly on Linux and macOS
(the __switch_context intrinsic) and the Fibers API on Windows. A task’s
stack is reserved address space with a guard page below it; only the pages
the task touches use memory, and a task that overflows its stack crashes the
program as a thread would. Stacks are reused for later tasks.
Each worker has its own ready queue, timers and backend. Workers talk
through a queue of messages each (a spawned task finished, was cancelled, or
was woken by a Mutex, Condition or Channel) and a doorbell that ends
a worker’s wait in its backend from another thread: an eventfd with epoll
and io_uring, a user event with kqueue, a posted completion with IOCP, and a
pipe (on Windows, a loopback UDP socket that a second socket sends to) with
poll(). run_blocking’s pool threads are plain threads that report back
through the same messages.
Current status
Filecalls other thanread,writeandwrite_all(opening, seeking,sync, directories) block the thread in a task.- On Windows, a
ThreadLocalhas a separate value in each task (it is built on fiber local storage); elsewhere the tasks of a thread share it.
The reference page documents every function.
Files and directories
The io module reads and writes files, lists and creates directories, and
inspects and changes what the file system records about them: permission
bits, timestamps and symbolic links. Every call that touches the file
system returns an IoResult, whose error says what went wrong portably
(is_not_found(), is_permission_denied(), is_already_exists(), …;
see Error handling with Result).
Pathis a file-system path. Joining,parent(),file_name(),extension()andnormalize()work on its text alone;exists(),metadata(),canonicalize()and theset_*methods below ask the file system.Fileis an open file.File.openreads,File.createwrites a new or emptied file, andOpenOptionsdescribes anything else (append, create only if missing, …).read_to_string,write_text,read_bytes,write_bytes,copy,renameandremovework on a whole file by path.Directorycreates and removes directories (create,create_all,remove,remove_all) and lists one, all at once withentries()or one entry at a time withfor.Metadatais a snapshot of a path’s kind, size, permission bits and timestamps, fromPath.metadata()(which follows symlinks),Path.symlink_metadata()(which does not) orFile.metadata().
# Write, read, list and remove files with io.
import io;
main(): int {
var folder = io.Directory.temp().join("pepper-files-basics-example");
io.Directory.remove_all(folder);
io.Directory.create_all(folder.join("notes")).value();
# Whole files at once.
var path = folder.join("notes").join("today.txt");
io.File.write_text(path, "buy milk\n").value();
assert(io.File.read_to_string(path).value() == "buy milk\n", "read back");
# An open File, with OpenOptions for anything but read or replace.
var log = io.OpenOptions().append(true).create(true).open(folder.join("log.txt")).value();
log.write_all("started\n").value();
log.close().value();
# Metadata, and a directory's entries.
var m = path.metadata().value();
assert(m.is_file && m.size == 9, "a 9-byte file");
var names = Array<String>();
for (var entry in io.Directory.open(folder).value()) {
names.append(entry.value().name);
}
assert(names.count == 2, "log.txt and notes");
io.Directory.remove_all(folder).value();
return 0;
}
Permissions
A file’s permission bits say who may read, write and run it. They are the
platform’s own bits, as Metadata.permissions reports them:
- On Linux and macOS, the POSIX
mode_tbits: read, write and execute for the owner, the group and everyone else, plus setuid, setgid and sticky. Pepper has no octal literals, sorwxr-xr-x(octal 755) is written 493, and a comment gives the octal. On a directory, read lets one list it, write lets one add and remove entries, and execute lets one reach anything inside it. - On Windows, the
FILE_ATTRIBUTE_*word. OnlyFILE_ATTRIBUTE_READONLYis a permission; whether a file runs depends on its extension. - WASI has no permission bits.
Three ways to change them:
set_permissions(mode)on aPathor an openFilesets the raw bits (chmodandfchmod). On Windows it applies only the read-only bit ofmode, and on WASI it returns an error.set_read_only(bool)is the portable read-only switch. On POSIX, read-only clears every write bit and writable sets only the owner’s, so this never opens a file to others. On Windows it sets or clearsFILE_ATTRIBUTE_READONLY.set_executable(bool)adds an execute bit wherever there is a read bit, aschmod +xdoes, or clears them all. It does nothing on Windows and WASI.
A file or directory can also be created with its bits, so that it never
exists with the default ones even for a moment: OpenOptions.mode(m) for a
new file, and Directory.create(path, mode = m) or create_all(path, mode = m) for a directory. The process umask still applies, as it does to open
and mkdir. These are ignored on Windows and WASI.
Timestamps
Metadata.modified and Metadata.accessed are nanoseconds since
1970-01-01 UTC. set_modified(time) and set_accessed(time) on a Path
or an open File set one and leave the other (utimensat and futimens;
SetFileTime on Windows, which keeps 100 ns). Writing to a file sets its
modification time, and adding or removing an entry sets a directory’s, so
set a time after the contents are final.
# Permission bits and timestamps: make a script executable, protect a file,
# and copy a modification time.
import io;
main(): int {
var folder = io.Directory.temp().join("pepper-files-permissions-example");
io.Directory.remove_all(folder);
# 448 is octal 700: only the owner may list or enter it (POSIX only).
io.Directory.create(folder, mode = 448).value();
# Created with mode 493 (octal 755, rwxr-xr-x) less the umask, so the
# script is never readable by others without being executable.
var script = folder.join("run.sh");
var file = io.OpenOptions().write(true).create_new(true).mode(493).open(script).value();
file.write_all("#!/bin/sh\necho hello\n").value();
file.close().value();
# The portable way: add an execute bit where there is a read bit.
var notes = folder.join("notes.sh");
io.File.write_text(notes, "#!/bin/sh\n").value();
notes.set_executable(true).value();
[Target(OS(Linux) || OS(Mac))] {
assert((notes.metadata().value().permissions & 64) != 0, "the owner may run it");
}
# Read-only: no write bits on POSIX, FILE_ATTRIBUTE_READONLY on Windows.
[Target(!OS(Wasi))] {
notes.set_read_only(true).value();
assert(notes.metadata().value().read_only, "read-only");
notes.set_read_only(false).value();
}
# Give the script the same modification time as the notes.
var modified = notes.metadata().value().modified;
script.set_modified(modified).value();
assert(script.metadata().value().modified == modified, "same time");
io.Directory.remove_all(folder).value();
return 0;
}
On Windows, an open File must have been opened for writing to change its
read-only bit or its times.
Symbolic links
File.symlink(target, link) and Directory.symlink(target, link) create
a symbolic link at link pointing to target. POSIX makes one kind of
link either way; Windows makes a file link and a directory link
differently, and a file link to a directory does not open, so use the one
that matches the target. The target is stored as given and need not exist;
a relative target is resolved from the directory holding the link.
Path.read_link() returns the target a link stores, without following it.
Path.symlink_metadata() describes the link itself, and metadata() what
it points to. Directory.remove_all removes links and never follows them.
# Symbolic links: make one to a file and one to a directory, and read them.
import io;
main(): int {
var folder = io.Directory.temp().join("pepper-files-symlinks-example");
io.Directory.remove_all(folder);
io.Directory.create_all(folder.join("releases").join("1.2")).value();
io.File.write_text(folder.join("app-1.2.conf"), "port = 80\n").value();
# Windows needs Developer Mode or an administrator to make symlinks.
[Target(!OS(Windows))] {
# A relative target is resolved from the folder holding the link.
var config = folder.join("app.conf");
io.File.symlink(io.Path("app-1.2.conf"), config).value();
assert(io.File.read_to_string(config).value() == "port = 80\n", "read through the link");
assert(config.read_link().value() == io.Path("app-1.2.conf"), "the stored target");
assert(config.symlink_metadata().value().is_symlink, "the link itself");
var current = folder.join("current");
io.Directory.symlink(io.Path("releases").join("1.2"), current).value();
assert(current.is_directory(), "a directory through the link");
}
# remove_all removes links, never what they point to.
io.Directory.remove_all(folder).value();
return 0;
}
Making a symlink on Windows needs Developer Mode or an administrator;
without either it fails with is_permission_denied().
Moving a file to another thread
A File is not thread-safe, so a thread cannot capture one. To hand an
open file to another thread, such as the pipe from a child process’s
output to a thread that reads it while this one writes to the child,
file.into_transfer() moves its handle into an io.FileTransfer, a
thread-safe token that a shared function can capture or a Channel can
carry, and io.File.adopt(transfer) takes it out again on the other
thread. The File it came from is closed afterwards, only the first adopt
gets the handle, and a token no one adopts closes the file when it goes.
A stream moves the same way, as a SocketTransfer (see
Moving a stream to another worker).
# Hand an open File to another thread with into_transfer and adopt.
import async;
import io;
main(): int {
var path = io.Directory.temp().join("pepper-files-transfer-example.txt");
io.File.write_text(path, "read on another thread").value();
var file = io.File.open(path).value();
# A File is not thread-safe; its transfer token is.
var transfer = file.into_transfer();
var size = async.Atomic<ulong>(0);
var reader = async.Thread.spawn(() => {
var mine = io.File.adopt(transfer).value();
size.store(mine.read_all().value().count);
});
reader.join();
assert(size.load() == 22, "the other thread read the file");
assert(!file.is_open(), "the File it came from is closed");
io.File.remove(path);
return 0;
}
Archives
archive.TarReader.extract uses all of the above: it restores each file’s
mode and modification time and makes the archive’s symlinks, as long as
they stay inside the directory it extracts into. See
Compression and archives.
Networking
The io module has TCP, UDP and Unix domain sockets, IP and socket
addresses, and host name lookup; the tls module encrypts TCP connections,
and the http module speaks HTTP over them.
In a plain program every call blocks the calling thread until it completes,
like File. To serve several connections at once, give each
one its own thread, or its own task: in a
task, a call that has to wait parks only that task.
On WebAssembly (--target wasm32-wasip1) there are no sockets, host name
lookups or TLS: each of those calls returns an IoError saying it is not
supported on WASI (see WebAssembly).
Addresses
An IpAddress is an IPv4 or IPv6 address. A SocketAddress is an IP
address and a port:
import io;
main(): int {
var home = io.IpAddress.v4(127, 0, 0, 1);
assert(home.is_loopback(), "127.0.0.1 is loopback");
# Text is read strictly and printed in one canonical form.
var six = io.IpAddress.parse("2001:DB8:0:0:0:0:0:1").value();
assert(six.to_string() == "2001:db8::1", "IPv6 prints compressed and lowercase");
assert(io.IpAddress.parse("256.1.1.1").error().is_invalid_input(), "256 is out of range");
var service = io.SocketAddress.parse("[::1]:8080").value();
assert(service.is_v6() && service.port() == 8080, "IPv6 with a port");
assert(service.to_string() == "[::1]:8080", "brackets around IPv6");
# A link-local IPv6 address names its interface after `%`: a number, or
# a name such as `eth0` that is looked up on this machine.
var link = io.SocketAddress.parse("[fe80::1%4000000000]:8080").value();
assert(link.address().scope_id() == 4000000000, "the scope id");
# A host name goes to the system resolver; an IP address is used as is.
var local = io.resolve("localhost", 80).value();
assert(local.count >= 1 && local[0].address().is_loopback(), "localhost is loopback");
return 0;
}
IpAddress.v4(a, b, c, d)andIpAddress.v6(a, ..., h)build an address from its bytes or its eight 16-bit groups.loopback_v4(),loopback_v6(),unspecified_v4()(0.0.0.0) andunspecified_v6()(::) are the well-known ones.IpAddress()is0.0.0.0.IpAddress.parsereads dotted decimal IPv4 and the RFC 4291 text form of IPv6, andto_string()prints the RFC 5952 form: lowercase, zeros compressed,::ffff:1.2.3.4for an IPv4-mapped address. Parsing and printing are the same on every platform.- An IPv6 address may carry a scope id, the network interface a link-local
address belongs to:
fe80::1%eth0orfe80::1%2(RFC 4007).scope_id()reads it (0 for none) andwith_scope_id(id)sets it. A name is looked up on this machine, andto_string()prints the interface’s name when it has one, else the number. Sockets pass it to the system assin6_scope_id. In a URI the%is escaped,http://[fe80::1%25eth0]/(RFC 6874); the HTTP client decodes it. SocketAddress(address, port)orSocketAddress.parse("1.2.3.4:80")make a socket address. IPv6 takes brackets:[::1]:80.resolve(host, port)looks a host name up with the system resolver (the hosts file and DNS) and returns every address it has, withportattached. An IP address is returned as is. A name with no address fails withis_not_found().
TCP
A TcpListener waits for connections. Each TcpStream is one connection:
import io;
# A server and a client on this machine, taking turns on one thread: the
# system completes a connection before the server accepts it.
main(): int {
# Port 0 asks the system for any free port.
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var address = listener.local_address().value();
var client = io.TcpStream.connect(address).value();
var (server, peer) = listener.accept().value();
assert(peer == client.local_address().value(), "accept reports who connected");
# The client sends a request and closes its sending side.
assert(client.write_all("ping").is_ok(), "send the request");
client.shutdown_write();
# The server reads until the client is done: read returns 0 at the end.
var request = Array<u8>();
while (server.read(request, 4096).value() > 0) {
}
assert(request.count == 4, "the whole request arrived");
# It answers, and the client reads exactly the bytes it expects.
server.write_all("pong");
var reply = Array<u8>();
assert(client.read_exact(reply, 4).is_ok(), "read the reply");
assert(reply[0] == 'p' && reply[1] == 'o', "the reply is pong");
return 0;
}
TcpListener.bind(address)listens on a local address. Port 0 lets the system pick a free port, andlocal_address()says which one it picked.accept()waits for the next connection and returns the stream and the peer’s address as a tuple.for (var connection in listener.incoming())accepts one connection per step, yielding anIoResult<TcpStream>(see Iteration).TcpStream.connect(address)connects to an address.TcpStream.connect_host(host, port)resolves a name and tries each of its addresses in turn.read(buffer, max)waits until some data arrives, then appends up tomaxbytes of it tobuffer, the same wayFile.readdoes. It returns 0 once the peer has closed its side.read_exact(buffer, count)loops untilcountbytes have arrived.write(data)sends what the system will take in one call.write_all(data)loops until everything is sent;datais au8[]or aString.shutdown_write()tells the peer that nothing more is coming, while this side can still read.set_no_delay(true)sends small writes immediately instead of combining them.local_address()andpeer_address()return the two ends of the connection.
Reading and writing any stream
A File, a TcpStream, a UnixStream and a tls.TlsStream have the
io.Reader and io.Writer traits, so a function that takes an
io.Reader (or <R: io.Reader>) reads from any of them:
import io;
# Any stream that is an io.Reader: a File, a socket, or a TLS stream.
count_lines(source: io.Reader): int {
var count = 0;
for (var line in source.lines()) {
if (line.is_err()) {
break;
}
count += 1;
}
return count;
}
# `print` and `print_line` write a value's text to any io.Writer, the way the
# global `print` writes to standard output.
main(): int {
var path = io.Directory.temp().join("networking-print.txt");
var file = io.File.create(path).value();
file.print("total: ");
file.print_line(42);
file.print_line((1, "two"));
file.close();
assert(io.File.read_to_string(path).value() == "total: 42\n(1, two)\n", "the file holds both lines");
assert(count_lines(io.File.open(path).value()) == 2, "a File is a Reader");
io.File.remove(path);
# The same calls work on a TcpStream (and a UnixStream).
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var client = io.TcpStream.connect(listener.local_address().value()).value();
var (server, peer) = listener.accept().value();
client.print_line($"hello from {client.local_address().value()}");
client.print_line("bye");
client.shutdown_write();
assert(count_lines(server) == 2, "a TcpStream is a Reader");
return 0;
}
io.Readerneedsread(buffer, max)and giveslines(), the stream’s lines asIoResult<String>s, andchunks(size), its bytes in arrays of at mostsize, both read as aforloop asks for them (see Iteration).io.Writerneedswrite(data)andwrite_all(data: u8[])and givesprint(value)andprint_line(value). They write the text thatto_stringmakes of any value, as the globalprintdoes on standard output;print_lineadds a\n. Both return anIoResult<void>, likewrite_all.io.ByteReader(bytes)reads an array of bytes as a stream, andio.ByteWriter()collects what is written in itsbytesarray, for code that takes a stream when the data is in memory.compress.DecompressReaderandcompress.CompressWriterwrap another stream (see Compression and archives).- Another type gets them by providing those methods and declaring
extend MyStream with io.Reader;orextend MyStream with io.Writer;. io.Print, which gave onlyprintandprint_line, is deprecated: useio.Writer.
UDP
A UdpSocket sends and receives datagrams. Each one arrives whole or not
at all, and in no guaranteed order:
import io;
main(): int {
var loopback = io.SocketAddress(io.IpAddress.loopback_v4(), 0);
var sender = io.UdpSocket.bind(loopback).value();
var receiver = io.UdpSocket.bind(loopback).value();
var datagram = Array<u8>();
datagram.append(1);
datagram.append(2);
datagram.append(3);
sender.send_to(datagram, receiver.local_address().value());
# One receive takes one whole datagram and says who sent it.
var buffer = Array<u8>();
var (count, from) = receiver.receive_from(buffer, 1500).value();
assert(count == 3 && buffer[2] == 3, "the datagram arrived whole");
assert(from == sender.local_address().value(), "from the sender");
return 0;
}
UdpSocket.bind(address)opens a socket on a local address.send_to(data, address)sends one datagram.receive_from(buffer, max)waits for one datagram, appends it tobuffer, and returns the byte count and the sender. The part of a datagram beyondmaxbytes is lost.- After
connect(address),sendandreceiveuse that peer, and datagrams from anyone else are dropped. set_broadcast(true)allows sending to a broadcast address.
Unix domain sockets
A Unix domain socket connects processes on the same machine through a path
in the file system instead of an IP address and port. UnixListener and
UnixStream work like TcpListener and TcpStream:
import io;
# A server and a client over a Unix domain socket, taking turns on one
# thread, as in the TCP example.
main(): int {
var path = io.Directory.temp().join("pepper-networking-unix.sock");
# Binding fails if the socket file is still there from an earlier run.
io.File.remove(path);
var listener = io.UnixListener.bind(path).value();
var client = io.UnixStream.connect(path).value();
var server = listener.accept().value();
assert(client.write_all("ping").is_ok(), "send the request");
var request = Array<u8>();
assert(server.read_exact(request, 4).is_ok(), "read the request");
server.write_all("pong");
var reply = Array<u8>();
assert(client.read_exact(reply, 4).is_ok(), "read the reply");
assert(reply[0] == 'p' && reply[1] == 'o', "the reply is pong");
# Closing the listener leaves the socket file; remove it when done.
listener.close();
assert(io.File.remove(path).is_ok(), "remove the socket file");
return 0;
}
UnixListener.bind(path)creates a socket file atpathand listens on it. Binding fails withis_address_in_use()if a file is already there, and closing the listener does not remove the file, so a server removes it withFile.removebefore binding and when it is done.UnixStream.connect(path)connects to it. It fails withis_not_found()when there is no socket file, and withis_connection_refused()when the file is there but nothing listens on it.accept()returns just the stream: the client’s end has no path.- Reading, writing,
shutdown_write(),into_transfer()andadoptare the same as on aTcpStream, andSharedUnixListeneris the counterpart ofSharedTcpListener. - A path holds at most 107 bytes (103 on macOS); a longer or empty one
fails with
is_invalid_input().
Unix datagram sockets
UnixDatagram is the Unix domain counterpart of UdpSocket, on Linux and
macOS only (Windows has no Unix datagram sockets, so it is declared with
[Target(OS(Linux) || OS(Mac))]):
# Unix datagram sockets: a server bound to a path answers each datagram.
import io;
main(): int {
# Linux and macOS only: Windows has no Unix datagram sockets.
[Target(OS(Linux) || OS(Mac))]
{
var folder = io.Directory.temp().join("pepper-udgram-example");
io.Directory.remove_all(folder);
io.Directory.create_all(folder).value();
var server_path = folder.join("server.sock");
var client_path = folder.join("client.sock");
var server = io.UnixDatagram.bind(server_path).value();
var client = io.UnixDatagram.bind(client_path).value();
client.send_to("ping".bytes(), server_path).value();
# Each receive takes one whole datagram and names its sender.
var request = Array<u8>();
var (n, sender) = server.receive_from(request, 1024).value();
assert(n == 4 && sender == client_path, "one datagram from the client");
server.send_to("pong".bytes(), sender).value();
var reply = Array<u8>();
client.receive(reply, 1024).value();
assert(reply.count == 4, "the reply");
# A pair is two sockets already connected to each other.
var (a, b) = io.UnixDatagram.pair().value();
a.send("hi".bytes()).value();
var got = Array<u8>();
assert(b.receive(got, 16).value() == 2, "through the pair");
# Closing leaves the socket files; remove them.
io.Directory.remove_all(folder).value();
}
return 0;
}
UnixDatagram.bind(path)creates a socket file to receive on, with the same path rules and errors asUnixListener.bind;unbound()makes one that can only send, andpair()two that are connected to each other.send_to(data, path)sends one datagram; it fails withis_not_found()when there is no socket file andis_connection_refused()when nothing is bound to it any more. A datagram is never dropped silently. While the receiver’s queue is full, a send waits on Linux; on macOS it fails at once with “No buffer space available” (ENOBUFS) and sends nothing, so try again later.receive_from(buffer, max)returns the byte count and the sender’s path, which is empty for an unbound sender. As with UDP, a datagram longer thanmaxis cut.connect(path),sendandreceivework as on aUdpSocket.
TLS
The tls module encrypts a TCP connection with TLS 1.2 or 1.3. A
TlsConnector makes client connections and checks the server’s
certificate. A TlsAcceptor serves them with a certificate and key. Both
produce a TlsStream, which reads and writes like a TcpStream:
import async;
import io;
import tls;
import "test_certificate";
# A TLS server and client on this machine, in two tasks.
main(): int {
var connector = tls.TlsConnector.create();
if (connector.is_err()) {
# On Windows, or where OpenSSL 3 is not installed.
print(connector.error().message);
return 0;
}
# A real server reads its certificate and key from files it was given.
var certificate = io.Directory.temp().join("pepper-example-cert.pem");
var key = io.Directory.temp().join("pepper-example-key.pem");
io.File.write_text(certificate, test_certificate());
io.File.write_text(key, test_private_key());
async.run(
() => {
var acceptor = tls.TlsAcceptor.create(certificate, key).value();
# Mutual TLS: only clients with a certificate the test
# certificate issued (here, the test certificate itself) get in.
assert(acceptor.require_client_certificate(certificate).is_ok(), "ask clients for certificates");
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var port = listener.local_address().value().port();
var server = async.Task<void>.spawn_local(() => {
var (tcp, peer) = listener.accept().value();
var stream = acceptor.accept(tcp).value();
var request = Array<u8>();
stream.read_exact(request, 4);
stream.write_all("pong");
stream.shutdown();
});
# The client trusts the test certificate as well as the system's.
var client = connector.value();
assert(client.add_root_certificate(certificate).is_ok(), "trust the test certificate");
assert(client.set_identity(certificate, key).is_ok(), "present a client certificate");
var stream = client.connect_host("localhost", port).value();
stream.write_all("ping");
var reply = Array<u8>();
while (stream.read(reply, 4096).value() > 0) {}
assert(reply.count == 4 && reply[0] == 'p' && reply[1] == 'o', "the reply is pong");
server.join();
}
);
io.File.remove(certificate);
io.File.remove(key);
return 0;
}
TlsConnector.create()trusts the system’s root certificates, andadd_root_certificate(path)adds more from a PEM file, such as a private certificate authority. Make one connector and share it: it is[Shared].connector.connect(tcp, host)runs the handshake over a connectedTcpStream. It checks that the certificate is valid and was issued forhost, which is also sent to the server (SNI).connect_host(host, port)connects the TCP stream first. An IP address inhostis checked against the certificate’s addresses.TlsAcceptor.create(certificate_chain, private_key)reads PEM files, andacceptor.accept(tcp)runs the handshake over a connection aTcpListeneraccepted.- Mutual TLS:
acceptor.require_client_certificate(roots)asks every client for a certificate and accepts only one issued by a certificate in the PEM fileroots;connector.set_identity(certificate_chain, private_key)gives the client a certificate to present. A client with no certificate, or an untrusted one, fails the server’saccept. With TLS 1.3 the client’sconnectcan succeed first, and its next read then fails or returns 0. - ALPN:
connector.set_protocols(["h2", "http/1.1"])offers protocols to servers, most preferred first, andacceptor.set_protocols(...)lists the ones a server accepts, also most preferred first. The handshake picks the first of the server’s that the client offers, andstream.protocol()names it on both sides. A client that offers no protocols connects with none chosen; one whose protocols the server accepts none of fails the handshake (theno_application_protocolalert of RFC 7301). read,read_exact,writeandwrite_allwork on the decrypted bytes.shutdown()sends the TLS close_notify, andreadreturns 0 once the peer has sent one or closed the connection.- A failed handshake returns an error whose message says why, such as
certificate verify failed: self-signed certificate.
On Linux and macOS the module uses OpenSSL 3, loaded when the first
connector or acceptor is made: install it with the system’s package
manager, or brew install openssl@3 on macOS. If it is missing,
create() returns an error. On Windows it uses the system’s SChannel,
which needs nothing installed; a private key (a server’s or a client’s)
must be PKCS #8 (BEGIN PRIVATE KEY), and TLS 1.3 needs Windows 11 or
Server 2022.
Timeouts
A socket call waits as long as it takes unless a timeout limits it:
import io;
# A read that gives up when the peer stays silent, and a connect with a
# limit of its own.
main(): int {
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var address = listener.local_address().value();
# connect_timeout gives up after 2 seconds instead of the system's limit.
var client = io.TcpStream.connect_timeout(address, 2000).value();
var (server, peer) = listener.accept().value();
# The server says nothing, so the read gives up after 100 milliseconds.
client.set_read_timeout(100).value();
var reply = Array<u8>();
var silent = client.read(reply, 4096);
assert(silent.is_err() && silent.error().is_timed_out(), "the read timed out");
# The stream is still open: once the server answers, the read succeeds.
server.write_all("pong");
assert(client.read_exact(reply, 4).is_ok(), "read the late reply");
return 0;
}
set_read_timeout(milliseconds)limits how long a read (or a UDP datagram receive) waits for data, andset_write_timeout(milliseconds)how long a write waits for room to send. A call that waits longer fails withis_timed_out(), and the socket stays open for another try. Each wait counts on its own, soread_exactorwrite_allcan take longer in all. 0, the default, means no limit;read_timeout()andwrite_timeout()return the current values.TcpStream,UdpSocket,UnixStreamandUnixDatagramhave them.TcpStream.connect_timeout(address, milliseconds)gives up on a connection that is not established in time, instead of after the system’s own limit, which is often minutes.- They work the same outside a task, where the thread blocks, and inside
one, where only the task parks. In a task,
async.timeoutalso limits every wait in its block, and whichever ends first wins. - A
TlsStreamwaits through itsTcpStream, so the TCP stream’s timeouts limit its reads, writes and handshake too. Set them beforeconnectoraccept.
Errors
Every operation returns an IoResult. Besides the file-system predicates,
IoError has these for networking:
| Predicate | When |
|---|---|
is_connection_refused() | nothing listens at the address |
is_connection_reset() | the peer reset or closed the connection (writing to a closed connection included) |
is_address_in_use() | another socket already has the address and port |
is_address_not_available() | the IP address is not one of this machine’s |
is_timed_out() | the peer never answered, or a read, write or connect timeout expired |
is_not_found() | resolve found no address for the name, or UnixStream.connect found no socket file |
Writing to a closed connection never raises SIGPIPE, so it cannot kill
the process. The write returns an error instead.
Closing
A socket closes when the last reference to it goes, like a File, and
close() closes it earlier and reports any error. A socket cannot be
copied, so it cannot go through a channel to another
thread. Use it on the thread that made it, or move it: into_transfer()
turns a TcpStream (or a UnixStream) into a thread-safe SocketTransfer
that TcpStream.adopt (or UnixStream.adopt) takes on another thread, and a SharedTcpListener is a
listener any thread can accept on (see
Tasks).
On POSIX a listener sets SO_REUSEADDR, so a server can restart right
away. On Windows it does not, because there that option would let another
program take over the port.
The reference pages for addresses, sockets and Unix domain sockets and TLS document every method.
HTTP
The http module has an HTTP/1.1 and HTTP/2 client and server, over plain
TCP or TLS. Both run in tasks: a request
that waits parks only its task. Over TLS the two sides pick HTTP/2 through
ALPN when both have it (see HTTP/2); the Request and Response
types, and the code that uses them, are the same for either version.
HTTP needs tasks and sockets, so it is not available on WebAssembly
(--target wasm32-wasip1, see WebAssembly).
import async;
import http;
import io;
# Answers every request: `/hello` greets, anything else is not found.
handle(request: http.Request): http.Response {
if (request.path() == "/hello") {
var response = http.Response(200).set_text("hello, " + request.query());
response.headers["Content-Type"] = "text/plain";
return response;
}
return http.Response(404).set_text("nothing at " + request.path());
}
main(): int {
async.run(
() => {
# Port 0 asks the system for any free port.
var server = http.Server.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var port = server.local_address().value().port();
# serve() runs until async.run ends, so it gets a task of its own.
async.spawn<void>(() => {
server.serve(handle);
});
var base = "http://127.0.0.1:" + to_string(port);
var client = http.Client();
var response = client.get(base + "/hello?pepper").value();
assert(response.status == 200, "found");
assert(response.text() == "hello, pepper", "the handler's text");
assert(response.headers["content-type"] == "text/plain", "names ignore case");
var missing = client.get(base + "/elsewhere").value();
assert(missing.status == 404 && missing.reason == "Not Found", "not found");
var posted = client.post(base + "/hello", "text/plain", "a body").value();
assert(posted.is_success(), "any method reaches the handler");
}
);
return 0;
}
Messages
- A
Requesthas amethod, aurl,headersand a body. For a client theurlis absolute (https://example.com/items?page=2); a server handler gets the request target (/items?page=2).uri()parses either as aUri, andpath()andquery()return its parts. - A
Responsehas astatus, areason,headersand a body.is_success()tests for a 2xx status. Headersmaps each field name to its values. Names ignore case (they are stored lowercased).headers["Accept"]reads the first value (""when there is none) andheaders["Accept"] = "text/html"replaces the field;get(name)returns an optional,addkeeps earlier values,values(name)returns them all, andnames()lists the fields in no defined order.set_textandset_bodyfill a request or response and return it, so they chain.body()returns the bytes, for any body including binary ones;text()decodes them as UTF-8, replacing invalid bytes with U+FFFD.set_reader(source)gives a request or response a body streamed from anyio.Reader(a file, acompress.CompressReader) instead of one held in memory; see Streaming bodies.
URLs
Uri (in the prelude, so no import is needed) parses any URI reference
following RFC 3986: an absolute URI with any scheme (https:, mailto:,
file:) or a relative reference such as a request target. It splits the
text into scheme, user_info, host, port, path, query and
fragment; the parts that can be missing are optionals. Components keep
their percent-escapes, and Uri.decode turns one into text; Uri.encode
escapes text for a path segment or query value, and Uri.encode_form and
Uri.decode_form write and read form data (see
Encodings and UUIDs).
base.resolve(reference) resolves a relative reference against a base URI,
as a browser resolves a link (RFC 3986 section 5.2).
main(): int {
# Any scheme parses; the scheme is lowercased and nothing else changes.
var uri = Uri.parse("HTTPS://user@example.com:8443/caf%C3%A9?page=2#top").value();
assert(uri.scheme == "https" && uri.host.value() == "example.com", "scheme and host");
assert(uri.port.value() == 8443 && uri.user_info.value() == "user", "port and user information");
assert(uri.path == "/caf%C3%A9" && uri.query.value() == "page=2", "path and query keep their escapes");
assert(Uri.decode(uri.path).value() == "/café", "decoded path");
assert(uri.path_and_query() == "/caf%C3%A9?page=2", "what an HTTP request line names");
# Missing parts are Empty: a mailto URI has no authority.
var mail = Uri.parse("mailto:someone@example.com").value();
assert(mail.host.is_empty() && mail.path == "someone@example.com", "no authority");
# A relative reference has no scheme, like a request target on a server.
var target = Uri.parse("/items?page=2").value();
assert(!target.is_absolute() && target.path == "/items", "relative");
# resolve() turns a relative reference, such as a redirect's Location,
# into the URI it names.
var page = Uri.parse("https://example.com/docs/guide/intro").value();
assert(page.resolve(Uri.parse("../api?v=2").value()).to_string() == "https://example.com/docs/api?v=2", "resolved");
assert(Uri.parse("http://example.com/a b").is_err(), "a space must be percent-encoded");
return 0;
}
Client
Client()trusts the system’s root certificates forhttps://URLs.Client(connector)uses atls.TlsConnectorof your own, for instance one that trusts a private certificate authority.get(url),post(url, content_type, body)andsend(request)open a connection, send the request and read the whole response, up toMAX_RESPONSE_BODY(64 MiB).open(request)returns at the header fields and leaves the body to read as a stream (see Streaming bodies). The client addsHost,Content-Length(orTransfer-Encoding: chunked) andConnection: close.- Any status is a successful result: check
status. An error means there was no response: the URL is nothttp://orhttps://(is_invalid_input()), the connection or TLS handshake failed, or the response was not valid HTTP. - Redirects (301, 302, 303, 307, 308) are followed, up to
max_redirects(10) per request, andresponse.urlis where the response came from. A 303, and a 301 or 302 after aPOST, becomes aGETwithout the body; a 307 or 308 repeats the method and body, except that a body fromset_readercannot be sent twice, so that redirect is returned. Leaving the origin dropsAuthorization,Proxy-AuthorizationandCookie.Client(0)returns every redirect as it is. - Every request from a
Clientuses a new connection; aSessionkeeps them open (see Reusing connections). - For
https://the client offers HTTP/2 and HTTP/1.1 through ALPN, on its own connector or the one it was given, and speaks whichever the server picks.
Server
Server.bind(address)listens for HTTP;Server.bind_tls(address, acceptor)serves HTTPS with atls.TlsAcceptor’s certificate.serve(handler)accepts connections on every worker ofasync.runand serves each in a task of its own, callinghandlerfor each request with the body read in full. It returns whenasync.runends. The handler is ashared (Request) => Response, since every worker calls it.- Connections stay open between requests unless the client asks to close
or speaks HTTP/1.0. Chunked request bodies and
Expect: 100-continueare understood. - The server adds
Content-Lengthto each response (or, for a body fromset_readerof unknown length,Transfer-Encoding: chunked), and the standard reason phrase whenreasonis empty (reason_phrase(status)returns it). - A request it cannot parse gets
400 Bad Request, header fields over 64 KiB get431, and a body overMAX_REQUEST_BODY(16 MiB) gets413. Server.bind_tlsoffers HTTP/2 and HTTP/1.1 through ALPN, and a plain-TCP client that opens with the HTTP/2 preface (prior knowledge, or h2c) is served HTTP/2 too. See HTTP/2.
Streaming bodies
A body need not fit in memory. Both directions stream through io.Reader,
the trait of files, sockets and the compress readers:
- Responses to a client:
client.open(request)returns theResponseas soon as its header fields arrive.response.reader()is then aBodyReaderthat reads the body from the connection as it arrives, with no size limit, undoing chunked framing, and ends where the body ends. Read it withread,chunksorlines, or wrap it in acompress.DecompressReader. The connection closes at the end of the body, atclose(), or when the last reference to the reader goes. - Requests from a client:
request.set_reader(source)sends the body chunked;set_reader(source, length)sends it withContent-Length. - Responses from a server: a handler returns
Response(200).set_reader(source), sent chunked to an HTTP/1.1 client (or withContent-Lengthwhen a length is given). An HTTP/1.0 client gets the body unframed, and the connection closes after it.
import async;
import http;
import io;
# `/download` streams a body of unknown length, which goes out chunked;
# `/upload` reports how many bytes it received.
handle(request: http.Request): http.Response {
if (request.path() == "/download") {
var data = "pepper\n".repeat(100000).bytes();
return http.Response(200).set_reader(io.ByteReader(data));
}
return http.Response(200).set_text(to_string(request.body().count));
}
main(): int {
async.run(
() => {
var server = http.Server.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var port = server.local_address().value().port();
async.spawn<void>(() => {
server.serve(handle);
});
var base = "http://127.0.0.1:" + to_string(port);
var client = http.Client();
# open() returns at the header fields; the body is read as it
# arrives and written to a file a chunk at a time.
var response = client.open(http.Request("GET", base + "/download")).value();
assert(response.headers["transfer-encoding"] == "chunked", "no length up front");
var path = io.Directory.temp().join("pepper-download.txt");
var file = io.File.create(path).value();
for (var chunk in response.reader().chunks(65536)) {
file.write_all(chunk.value());
}
file.close();
# Send the file back as a request body read from the file.
var upload = http.Request("PUT", base + "/upload").set_reader(io.File.open(path).value());
assert(client.send(upload).value().text() == "700000", "every byte arrived");
io.File.remove(path);
}
);
return 0;
}
Reusing connections
client.session() returns a Session, which sends requests with the
client’s settings and keeps each connection open after its response
(HTTP/1.1 keep-alive). The next request to the same scheme, host and port
uses it again, saving the TCP and TLS handshakes:
import async;
import http;
import io;
main(): int {
async.run(
() => {
var server = http.Server.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var port = server.local_address().value().port();
async.spawn<void>(() => {
server.serve((request) => {
return http.Response(200).set_text("page " + request.query());
});
});
var base = "http://127.0.0.1:" + to_string(port);
var session = http.Client().session();
for (var page in 1..>3) {
var response = session.get(base + "/items?" + to_string(page)).value();
assert(response.text() == "page " + to_string(page), "each page");
}
# One connection carried all three requests, and waits for more.
assert(session.idle_count == 1, "kept open");
session.close();
}
);
return 0;
}
- A connection goes back to the session when its response body has been
read to its end (
sendreads it; foropen, read theBodyReaderto its end), unless the server saidConnection: close. max_idle_per_host(4) connections wait for each scheme, host and port; one idle longer thanidle_timeout(90 seconds) is closed instead of used.idle_countsays how many wait, andclose()closes them.- A request that finds its reused connection closed by the server goes
again on a new one, unless its body came from
set_reader. - An HTTP/2 connection is not idle and taken in turn: every request to its
origin shares it at once, each on a stream of its own.
idle_countcounts it while no request is using it. - A
Sessionis not[Shared]: use one per thread or task. AClientis shared, and connections, which belong to one thread, cannot be. Tasks started withspawn_localmay share a session, and over HTTP/2 their requests go out on one connection at the same time.
HTTP/2
HTTP/2 (RFC 9113) carries many requests on one connection at once, as streams, with header fields compressed (HPACK, RFC 7541) and each body flow-controlled so that one large transfer does not hold up the rest.
- Choosing it: over TLS, the client offers
h2andhttp/1.1through ALPN andServer.bind_tlsaccepts both, preferringh2, so two Pepper programs speak HTTP/2 with no setting to change. A server or client that offers only HTTP/1.1 gets HTTP/1.1. On plain TCP the client speaks HTTP/1.1; the server also takes HTTP/2 from a client that starts with its preface (prior knowledge). - Client: a
Sessionkeeps one HTTP/2 connection per origin, and requests from several tasks share it at once. A request outside a session opens a connection that closes after its body. A connection the server ends with GOAWAY is replaced by a new one for the next request. - Server: each stream is a request, answered by the same handler in a
task of its own, so the requests of one connection run at the same time.
The handler sees the
Hostfield from:authority. A server lets a client open 100 streams at once. - Messages: field names go lowercase on the wire, as HTTP/2 requires,
and fields that only mean something to HTTP/1.1 (
Connection,Transfer-Encoding,Upgrade,Keep-Alive) are left out. HTTP/2 has no reason phrase, so a response’sreasonis the standard one for its status. Bodies stream throughio.Readerexactly as for HTTP/1.1; closing a response’s reader early resets only its stream.
Current status
- A server handler gets the request body read into memory in full, up to
MAX_REQUEST_BODY(16 MiB). - HTTP/2 server push, stream priorities and the
Upgrade: h2chandshake are not supported; a client speaks HTTP/2 only over TLS.
The reference pages for messages, the
client, the
server and Uri
document every method.
Processes
The process module starts other programs and holds what belongs to the
running one: its arguments, its exit, its id, its executable, and its working
directory. Bring it in with import process.
On WebAssembly (--target wasm32-wasip1) a program cannot start another:
run, spawn, exec and executable_path return an IoError saying it
is not supported on WASI. Its arguments, exit and environment work (see
WebAssembly).
Running a program
Command describes a program to start. Its setters return the same
Command, so they chain. run() starts the program, waits for it to end,
and returns an Output: the status it ended with, and the text it wrote to
stdout and stderr.
import io;
import process;
# Runs a copy of this program as a child, so the example needs no other
# program on any system: the child's first argument says what to do.
main(args: String[]): int {
if (args.count > 1) {
io.File.stdout().write_all("hello from " + args[1]);
io.File.stderr().write_all("a warning");
return 3;
}
var self = process.executable_path().value().to_string();
var output = process.Command(self).argument("the child").run().value();
assert(output.stdout == "hello from the child", "standard output is collected");
assert(output.stderr == "a warning", "and so is standard error");
assert(!output.status.success(), "the child failed");
assert(output.status.code().value() == 3, "with exit code 3");
# A program that is not found fails to start.
var missing = process.Command("no-such-program").run();
assert(missing.is_err() && missing.error().is_not_found(), "not found");
return 0;
}
- The program is found the way a shell finds it. A bare name is searched for
on the
PATH(on Windows, also next to the running program and in the system directories, with.exeadded when the name has no extension). Anything with a path separator is a path. - Each
argumentreaches the program as one argument, whatever it holds. No shell splits it or expands*or$NAME. On Windows, where a program receives a single command line, each argument is quoted so that the C runtime splits it back out as it was. run()collects standard output and error, both at once, so a child that fills one pipe while the parent reads the other does not stall. Standard input is the null device unless it was set. The output is decoded as UTF-8, with each invalid byte sequence replaced by U+FFFD; read a child’s binary output from a pipe withspawn()instead.- A program that cannot be started is an error (
is_not_found()when it does not exist). A program that runs and fails is a successful result whosestatussays so.
exec() runs the program in place of the running one, as a shell’s exec
does: the program’s exit status becomes this process’s own, and exec()
returns only when the program cannot be started, with the error. On POSIX
the program takes over the process; Windows cannot replace a process, so
there the program is started, waited for, and its exit status passed on.
Output that print buffered is flushed first, and the standard streams must
stay inherited (otherwise the error has is_invalid_input()).
Exit status
ExitStatus.success() is true for exit code 0. code() returns the exit
code, and signal() the POSIX signal that ended the process (such as 9 for
SIGKILL). Each is an optional: a process killed by a signal has no exit
code, and a process that exited has no signal. Windows has no signals, so
there every process has a code; a POSIX parent sees only the low 8 bits of
it.
Talking to a child
spawn() starts the program and returns a Child at once. Each of the three
standard streams is Stream.Inherit (shared with the parent, the default),
Stream.Pipe (connected to the parent), or Stream.Null. A piped stream is
an io.File from child.stdin(), child.stdout() or child.stderr(), so
the file read and write methods apply.
import io;
import process;
main(args: String[]): int {
if (args.count > 1) {
# The child: read standard input to the end, and write it back in
# capitals.
var text = io.File.stdin().read_string().value();
io.File.stdout().write_all(text.upper());
return 0;
}
var self = process.executable_path().value().to_string();
var child = process.Command(self)
.argument("upper")
.stdin(process.Stream.Pipe)
.stdout(process.Stream.Pipe)
.spawn()
.value();
child.stdin().write_all("pear and apple");
child.stdin().close(); # the child reads to the end, so end it
var shouted = child.stdout().read_string().value();
var status = child.wait().value();
assert(shouted == "PEAR AND APPLE", "the child's output");
assert(status.success(), "the child succeeded");
return 0;
}
- Close a piped standard input when you are done writing, or a child that
reads to the end never finishes.
wait()closes it too, before waiting. - Read piped output before waiting. A child that fills a pipe stops until the parent reads it, so waiting first can wait forever.
wait()returns the exit status, and again on later calls.try_wait()returns none while the child runs.kill()ends the child at once (SIGKILL, orTerminateProcesson Windows); wait for it afterwards.- Wait for every child. On POSIX a child that ended stays in the process
table until it is waited for. Releasing a
Childneither waits for it nor kills it.
In a task, run(), wait() and reads from a pipe park the task
rather than block its worker, so other tasks keep running.
Environment and working directory
A child inherits the parent’s environment. environment(name, value) sets a
variable for the child, remove_environment(name) leaves one out, and
clear_environment() starts from an empty environment. On Windows, names
ignore case. working_directory(path) starts the child in another
directory. The running process reads and changes its own environment with
os.environment(name) and os.set_environment(name, value) (see
The operating system).
import io;
import os;
import process;
main(args: String[]): int {
if (args.count > 1) {
io.File.stdout().write_all(os.environment("GREETING").value_or("unset"));
return 0;
}
var self = process.executable_path().value().to_string();
var greeted = process.Command(self).argument("child").environment("GREETING", "hello").run().value();
assert(greeted.stdout == "hello", "the child sees the variable");
var cleared = process.Command(self).argument("child").clear_environment().run().value();
assert(cleared.stdout == "unset", "an empty environment");
# The child starts in another directory; the parent's stays as it was.
var before = process.working_directory().value();
var elsewhere = process.Command(self).argument("child").working_directory(io.Directory.temp()).run();
assert(elsewhere.is_ok(), "started in the temporary directory");
assert(process.working_directory().value() == before, "unchanged");
return 0;
}
The running process
process.arguments()returns the program’s command-line arguments, the same arraymain(args: String[])receives (see Functions).process.exit(code)ends the program at once with statuscode.process.id()is the process’s id.process.executable_path()is the absolute path of the running program’s executable, so a tool can find files installed next to it.process.working_directory()andprocess.change_working_directory(path)read and change the directory relative paths are resolved against, asio.Directory.current()andio.Directory.set_current(path)do.
arguments() and exit() without import process still work, but are
deprecated and warn.
Signals
process.signals([...]) returns an async.Channel<Signal> that receives
each signal it names instead of the signal’s default action, which ends the
program. A server or tool waits on it to shut down cleanly: stop accepting
work, flush, remove temporary files.
Signal | POSIX | Windows |
|---|---|---|
Interrupt | SIGINT (Ctrl-C) | CTRL_C_EVENT, CTRL_BREAK_EVENT |
Terminate | SIGTERM | CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT, CTRL_SHUTDOWN_EVENT |
Hangup | SIGHUP | never sent |
import process;
import [Signal] from process;
main(): int {
var stop = process.signals([Signal.Interrupt, Signal.Terminate]);
# Serve until Ctrl-C or a request to terminate arrives: here, for at most
# a tenth of a second.
var signal = stop.receive_for(100);
if (signal.is_ok()) {
print("shutting down cleanly");
}
# Closing the channel gives the signals back their default action.
stop.close();
return 0;
}
No program code runs inside the operating system’s signal handler: on POSIX
the handler only writes the signal’s number to a pipe, and a runtime thread
sends it on the channel. So the channel is the whole interface, and works as
any channel does: receive() waits, a task can select on it alongside its
other channels, and try_receive() polls between steps of a long job.
- Every open channel that names a signal receives it.
- Closing a channel stops it receiving; once no open channel names a signal, its default action returns.
- On Windows the system ends the program a few seconds after
Terminate(the console closing, a log-off or a shutdown) whatever it does, so keep that cleanup short. - WebAssembly has no signals: the channel never receives.
- Writing to a socket whose peer has gone returns an
IoErrorrather than raisingSIGPIPE; the runtime asks for that on every socket.
Finding a program
process.find_executable(name) looks a program up on the PATH as a shell
does, returning the path of the first match that may be run, or none, so a
tool can say what to install before trying to run it.
process.is_executable(path) tells whether a file may be run.
var perf = process.find_executable("perf");
if (perf.is_empty()) {
print("perf is not installed");
}
See the process module reference.
The operating system
The os module reports what the operating system provides beyond the
running process: environment variables, clocks, and facts about the
machine and the user. Bring it in with import os. The full API is in the
reference.
Environment variables
os.environment(name) returns a variable’s value as a String?: none when
it is not set, and an empty String when it is set to nothing.
os.environment_variables() returns them all as a HashMap<String, String>
from name to value.
import os;
main(): int {
# An optional: none when the variable is not set.
var runtime = os.environment("PEPPER_RUNTIME").value_or("runtime");
assert(!runtime.is_empty, "a fallback when unset");
assert(os.environment("PEPPER_SURELY_NOT_SET").is_empty(), "unset");
var count = 0;
for (var (name, value) in os.environment_variables()) {
if (name.starts_with("PEPPER_")) {
print($"{name}={value}");
}
count = count + 1;
}
assert(count > 0, "a process has an environment");
return 0;
}
- On Windows, names ignore case (
PathfindsPATH), and values are read as UTF-16, so text outside the ANSI code page survives. - On POSIX a value is whatever bytes the parent passed; one that is not valid UTF-8 is returned as it is.
Changing the environment
os.set_environment(name, value) sets a variable for the running process,
and os.remove_environment(name) removes one. Later reads see the change,
and so do the children the process starts afterwards.
import os;
main(): int {
var name = "PEPPER_EXAMPLE_GREETING";
os.set_environment(name, "hello");
assert(os.environment(name).value() == "hello", "set");
os.set_environment(name, "");
assert(os.environment(name).value() == "", "set to nothing");
os.remove_environment(name);
assert(os.environment(name).is_empty(), "removed");
return 0;
}
- Every read and change the runtime makes goes through one lock, so Pepper code never races with itself over the environment, from any thread.
- C code that calls
getenvitself is outside that lock. On POSIX,setenvraces with such a call on another thread (Rust 2024 marks itsset_varunsafefor this reason), so set variables before starting threads that run C code reading them. - A name that is empty or contains
=or a NUL byte, or a value that contains a NUL byte, panics. - On Windows names ignore case, and the C runtime’s own
getenvcopy does not see the change. On WASI the change stays inside the program. - To give only a child a different environment, use
process.Command’senvironment,remove_environmentandclear_environment(see Processes).
Time
Three value types cover time. Each is a value struct:
copied, compared with ==, < and the other comparisons, and never
allocated.
Durationis a span of time in nanoseconds, never negative. Make one withfrom_seconds,from_milliseconds,from_microsecondsorfrom_nanoseconds, read it back withseconds(),milliseconds()and the others (rounded down), and add, subtract, multiply or divide it. Printing one shows it in the largest unit it reaches:2.5s,15ms,40ns.Instantreads a monotonic clock, for measuring how long something takes. It only moves forward, whatever the system’s date is set to.later - earlieris theDurationbetween two instants, andstart.elapsed()the time since one.SystemTimereads the wall clock: nanoseconds since the Unix epoch (1970-01-01 00:00:00 UTC).unix_seconds()is what file systems and most protocols record. Since the wall clock can be set back,duration_since(earlier)is an optional: none whenearlieris later. Thetimemodule turns it into a date and time, in UTC or a time zone, and back.
import os;
main(): int {
# Measure with the monotonic clock: subtracting two instants gives a
# Duration.
var start = os.Instant.now();
long total = 0;
for (var i in 0..100000) {
total = total + i;
}
var elapsed = os.Instant.now() - start;
print($"summed in {elapsed}"); # such as "1.234ms"
var timeout = os.Duration.from_seconds(2) + os.Duration.from_milliseconds(500);
assert(timeout.milliseconds() == 2500, "whole milliseconds");
assert(timeout.to_string() == "2.5s", "the largest unit it reaches");
assert(elapsed < timeout, "durations compare");
# The wall clock counts from the Unix epoch.
var now = os.SystemTime.now();
assert(now.unix_seconds() > 1700000000, "after November 2023");
var tomorrow = now + os.Duration.from_seconds(86400);
assert(tomorrow.duration_since(now).value() == os.Duration.from_seconds(86400), "a day later");
assert(now.duration_since(tomorrow).is_empty(), "no negative durations");
return 0;
}
Subtracting a longer Duration, or a later Instant, panics: neither can
be negative.
The machine and the user
import os;
main(): int {
print($"{os.system_name()} {os.system_version()} on {os.architecture()}");
print($"host {os.hostname()}: {os.logical_cpus()} logical processors, {os.physical_cpus()} cores");
print($"{os.total_memory() / 1048576} MiB of memory in {os.page_size()}-byte pages");
var user = os.user_name().value_or("unknown");
var home = os.home_directory();
print($"user {user}, home {home.is_value() ? home.value().to_string() : "unknown"}");
print($"temporary files go in {os.temp_directory()}");
var executable = os.system_os() == os.OperatingSystem.Windows ? "tool.exe" : "tool";
assert(executable.starts_with("tool"), "picked a name");
return 0;
}
| Function | Returns |
|---|---|
system_os() | the OperatingSystem the program was built for: Linux, Mac, Windows or Wasi |
system_name() | the same as text: "linux", "macos" or "windows" |
system_version() | the kernel release on Linux, the product version on macOS (14.5), major.minor.build on Windows |
architecture() | "x64", "aarch64" or "wasm32", as [Target(Arch(...))] names them |
hostname() | the machine’s host name |
logical_cpus(), physical_cpus() | processor counts, the same as async’s, capped by a Linux container’s CPU quota |
total_memory(), page_size() | physical memory, or a Linux container’s lower memory limit, and the virtual memory page size, in bytes |
user_name() | the user the program runs as, or none |
home_directory() | HOME (on Windows USERPROFILE), or the user database’s entry, or none |
temp_directory() | the directory for temporary files, the same as io.Directory.temp() |
is_elevated() | whether the program runs as root, or elevated (“Run as administrator”) on Windows; never on WASI |
Containers
On Linux, total_memory() and logical_cpus() report what a container
lets the program use, as the JVM and Go do, so a pool or a cache sized from
them fits inside it. Both read the process’s cgroup and each cgroup above
it, under /sys/fs/cgroup, with cgroup v2 or v1:
total_memory()returns the smallest of the physical memory and every memory limit found (memory.max, or v1’smemory.limit_in_bytes), asdocker run --memorysets it.logical_cpus()returns the smaller of the online processors and the smallest CPU quota (cpu.max, or v1’scpu.cfs_quota_usovercpu.cfs_period_us), rounded up to whole processors:docker run --cpus=1.5counts as 2.physical_cpus()is never more than that.
Where no limit is set, or the files cannot be read, they report the machine.
Current limitations
async.timeoutstill takes milliseconds, not aDuration.
Dates, times and time zones
The time module turns instants into calendar dates and times of day, and
back: dates and times with no zone, fixed offsets from UTC, time zones with
daylight saving time, and the text forms of RFC 3339, ISO 8601, HTTP and
C’s strftime. Bring it in with import time. The full API is in the
reference.
An instant itself is an os.SystemTime: nanoseconds since
the Unix epoch. time says what date and time that is somewhere, and
which instant a date and time names.
The calendar is the proleptic Gregorian calendar, which extends today’s rules to years before it was adopted, and leap seconds are ignored, as Unix time, Go and Java ignore them: every day has 86400 seconds.
Dates and times of day
Four value types carry no time zone. Each is copied,
compared with ==, < and the other comparisons, and never allocated.
Dateis a year, a month (1 to 12) and a day. It knows itsweekday(),day_of_year()and whether its yearis_leap_year(), and moves withadd_days,add_monthsandadd_years. A month or year later keeps the day when it can and otherwise takes the month’s last day, so a month after January 31 is February 28 or 29.Timeis an hour, minute, second and nanosecond within a day.DateTimeis aDateand aTimetogether: what TOML calls a local date-time. Adding or subtracting anos.Durationmoves it.Weekdayis an enum,MondaytoSunday, whose values are ISO 8601’s numbers, 1 to 7.
import os;
import time;
main(): int {
# A Date is a day on the calendar, with no time of day or zone.
var leap_day = time.Date(2024, 2, 29);
assert(leap_day.weekday() == time.Weekday.Thursday, "2024-02-29 was a Thursday");
assert(leap_day.day_of_year() == 60, "the 60th day of the year");
assert(leap_day.add_years(1) == time.Date(2025, 2, 28), "no February 29 in 2025");
assert(time.Date(2024, 1, 31).add_months(1) == leap_day, "the last day of February");
assert(time.Date(2024, 12, 25).days_since(leap_day) == 300, "days between dates");
# Parts from outside the program are checked, not trusted.
assert(time.Date.checked(2023, 2, 29).is_empty(), "2023 is not a leap year");
assert(time.Date.parse("2024-02-29").value() == leap_day, "ISO 8601 text");
# A DateTime adds a time of day; durations move it.
var start = time.DateTime(2024, 3, 1, 23, 30);
var end = start + os.Duration.from_seconds(90 * 60);
print(end); # 2024-03-02T01:00:00
assert(end.date == time.Date(2024, 3, 2) && end.time == time.Time(1, 0), "past midnight");
assert(start < end, "date times compare");
return 0;
}
The constructors (Date(2024, 2, 29)) panic on parts that name no date or
time, which suits values written in the program. For parts that come from
outside it, checked returns an optional instead, and parse a
Result. Years run from -999999 to 999999, and year 0 is
1 BC.
Offsets and timestamps
Offset is a fixed offset from UTC, east positive: Offset.from_hours(5, 30) is +05:30. OffsetDateTime is a DateTime at an Offset, which
names one instant: RFC 3339’s timestamp and TOML’s offset date-time.
import time;
main(): int {
# An OffsetDateTime is an RFC 3339 timestamp: a date and time at an
# offset from UTC, which together name one instant.
var meeting = time.OffsetDateTime.parse("2024-03-01T09:00:00+01:00").value();
print(meeting.to_utc()); # 2024-03-01T08:00:00Z
assert(meeting.offset == time.Offset.from_hours(1), "the offset it was written at");
# The same instant at two offsets is not ==, but compares as equal.
var utc = meeting.to_utc();
assert(utc != meeting && !(utc < meeting) && !(utc > meeting), "one instant");
assert(utc.to_system_time() == meeting.to_system_time(), "one SystemTime");
# strftime codes lay a date out any way.
assert(meeting.format("%A %e %B %Y, %H:%M %:z") == "Friday 1 March 2024, 09:00 +01:00", "strftime");
assert(meeting.format("%G-W%V-%u") == "2024-W09-5", "the ISO week date");
# HTTP's Date and Last-Modified headers, both ways.
var header = meeting.to_http_date();
assert(header == "Fri, 01 Mar 2024 08:00:00 GMT", "an IMF-fixdate, in GMT");
assert(time.OffsetDateTime.parse_http_date(header).value() == utc, "and back");
return 0;
}
OffsetDateTime.from_system_time(t, offset)is the instanttatoffset(UTC by default), andto_system_time()goes back.to_offsetandto_utcshow the same instant at another offset.==compares the date, time and offset, so the same instant written at two offsets is not==.<,<=,>and>=compare instants.DateTime.from_system_time(t)andto_system_time()read aDateTimeas UTC.
Text
Each type’s to_string() writes ISO 8601, and its static parse reads it:
| Type | Text |
|---|---|
Date | 2024-02-29; outside the years 0 to 9999, signed: -0001-12-31, +10000-01-01 |
Time | 08:30:00, with the fraction of a second when there is one: 08:30:00.25 |
DateTime | 2024-03-01T09:00:00 |
Offset | +01:00; parse also takes Z, +0100 and +01 |
OffsetDateTime | 2024-03-01T09:00:00+01:00, Z for UTC |
parse also reads what TOML allows: a space or t for the T, z for
the Z, and a time without its seconds (09:00). Digits of a fraction
past the nanosecond are dropped.
format(pattern) lays a value out with strftime’s % codes, such as
%Y-%m-%d, %A, %B and %H:%M; the module
reference lists them all. Names are English
whatever the locale. %z, %:z and %s need an offset, so only an
OffsetDateTime takes them; an unknown code panics.
HTTP dates (RFC 9110) go both ways: to_http_date() writes the
IMF-fixdate of Date and Last-Modified headers, Fri, 01 Mar 2024 08:00:00 GMT, and OffsetDateTime.parse_http_date reads it and the two
obsolete forms servers must still accept, RFC 850’s and C’s asctime.
Time zones
A TimeZone gives the offset from UTC at each instant in one place:
TimeZone.utc(), andTimeZone.fixed(offset)for an offset that never changes.TimeZone.local(), the system’s zone: theTZenvironment variable or/etc/localtimeon Linux and macOS, as the C library’slocaltimereads them, and the zone in Windows’ settings. WebAssembly has no local zone, so it is UTC there.TimeZone.named("Europe/Paris"), a zone of the IANA time zone database, with its daylight saving time and history, or an error when the system does not know the name.
zone.offset_at(t) is the offset at the instant t, and zone.at(t) its
date and time there. zone.at_local(date_time) goes the other way, from
a date and time on the zone’s clocks to the instant. Where the clocks go
back, a time happens twice, and at_local picks the earlier; where they go
forward, a time never happens, and it moves on by the length of the gap.
time.now() is the current date and time in the local zone, and
time.now_utc() in UTC.
import os;
import time;
main(): int {
# The local zone and UTC are always there.
var now = os.SystemTime.now();
print($"here: {time.TimeZone.local().at(now)}");
print($"UTC: {time.now_utc()}");
# Named zones come from the system's time zone database.
var found = time.TimeZone.named("Europe/Paris");
if (found is Ok(paris)) {
var bastille = time.OffsetDateTime.parse("2024-07-14T10:00:00Z").value();
assert(paris.at(bastille.to_system_time()).to_string() == "2024-07-14T12:00:00+02:00", "summer time");
# A local time in the zone gets the offset in effect then.
var christmas = paris.at_local(time.DateTime(2024, 12, 25, 18, 0));
assert(christmas.offset == time.Offset.from_hours(1), "winter time");
# 02:30 never happens on the night the clocks go forward: it moves
# on by the hour that was skipped.
var skipped = paris.at_local(time.DateTime(2024, 3, 31, 2, 30));
assert(skipped.to_string() == "2024-03-31T03:30:00+02:00", "the gap");
}
assert(time.TimeZone.named("Nowhere/Atlantis").is_err(), "an unknown zone");
return 0;
}
Nothing is bundled: named zones come from the system.
- On Linux and macOS, the TZif files under
/usr/share/zoneinfo, or the folder theTZDIRenvironment variable names. A minimal Linux container may have none until itstzdatapackage is installed. The rule at the end of each file covers instants after its last listed change. - On Windows, ICU (
icu.dll, part of Windows 10 version 1903 and later), which knows the same names, or the TZif files in the folderTZDIRnames when it is set. - On WebAssembly, TZif files under
/usr/share/zoneinfo(orTZDIR) when the host lends the program that folder.
Current limitations
formatwrites text but nothing reads astrftimepattern back; parse ISO 8601, RFC 3339 and HTTP dates with theparsefunctions.- Names in
formatare English only, and there are no locale formats (%c,%x,%X) or zone abbreviations (%Z). - On Android, named zones are not found: its database is one packed file, not a folder of TZif files.
Command-line options
The cli module parses a program’s command line. Declare each option and
positional argument on a cli.Parser, call parse, then read the values
through the handles the declarations returned, or declare them all at once
as the fields of a struct. The help, --version and
the error messages all come from the declarations. Bring it in with
import cli; the full API is in the reference.
# Command-line parsing: declare options and arguments, then parse.
import cli;
main(args: String[]): int {
var parser = cli.Parser("greet", "Greets someone", version = "1.0");
var greeting = parser.option("greeting", "g", "what to say", default_value = "hello");
var loud = parser.flag("loud", "l", "shout it");
var times = parser.option<int>("times", "n", "how many times to say it", value_name = "COUNT", default_value = "1");
var names = parser.positionals("names", "who to greet");
# `parse` prints --help and --version and exits; on a mistake it writes
# the error and the usage to standard error and returns false.
if (!parser.parse(args)) {
return 2;
}
var people = names.is_set ? names.values() : ["world"];
for (var person in people) {
var line = $"{greeting.value()}, {person}";
for (var _ in 0..times.value()) {
print(loud.is_set ? line.upper() : line);
}
}
return 0;
}
greet --help prints:
Greets someone
usage: greet [OPTIONS] [names]...
Arguments:
<names>... who to greet
Options:
-g, --greeting <GREETING> what to say [default: hello]
-l, --loud shout it
-n, --times <COUNT> how many times to say it [default: 1]
-h, --help show this help
-V, --version show the version
The raw arguments stay where they were: main(args: String[]) receives
them, and process.arguments() returns them. parse takes them as main
receives them, program name first.
Declaring
| Declaration | Handle | Reads |
|---|---|---|
flag(name, short_name, help, global) | Flag | is_set and occurrences (properties) |
option<T>(name, short_name, help, value_name, default_value, required, global, implicit_value) | Argument<T> | value(), value_or(fallback), is_set |
multiple<T>(name, short_name, help, value_name, required, global) | ArgumentList<T> | values(), is_set |
positional<T>(name, help, required, default_value) | Argument<T> | value(), value_or(fallback), is_set |
positionals<T>(name, help, required) | ArgumentList<T> | values(), is_set |
T is the type of the value (see Typed values); leave it
out for a String: option("output") is option<String>("output").
- An option’s
nameis its long name without--;short_nameis one character without-, or empty for none. One of the two may be empty:option("", "o")is an option spelled only-o, as a C compiler’s-I,-Dand-oare. Pass the rest by name:option("output", "o", "the executable to write", default_value = "a.out"). value()returns the value given, else the default; it panics when the argument was left out and has no default, so usevalue_oror checkis_setfor an optional one.is_set(a property, likeoccurrencesandis_selected) is false for a value that came from the default.- Positional arguments fill in declaration order. A required one cannot
follow an optional one, and
positionalstakes every value that is left, so it comes last. -h/--helpis always there, and-V/--versionwhen the parser has aversion. Declaring-hor-Vyourself takes the short name; the long forms keep working.- A mistake in the declarations, such as a name declared twice or a short name longer than one character, panics: it is a bug in the program, not in its input.
On the command line
- A value follows its option as
--output a.out,--output=a.out,-o a.out,-oa.outor-o=a.out. It is taken as it is, even when it starts with-. - Short flags combine:
-vqis-v -q, and-vo a.outgives-vand then-o a.out.-vvvcounts threeoccurrences. - Options and positional arguments mix in any order.
--ends the options, sorm -- -fileremoves a file named-file, and-alone is a positional argument (by custom, standard input). - An option that takes one value keeps the last one given.
- An option declared with an
implicit_valuetakes a value only when it is attached (--emit-ast=json,-O3); given alone, it has the implicit value, and the argument after it is not its value. The help shows it as--emit-ast[=<FORMAT>]or-O[<LEVEL>].
Typed values
A declaration with a type argument reads its values as that type, and a value that is not one fails the parse:
greet: error: invalid value 'x' for '--times': expected an integer
T | A value is | An error expects |
|---|---|---|
String | any text | |
int, long | an integer, as int.parse reads it | an integer |
uint, ulong | an integer that is not negative | a non-negative integer |
byte, short, ushort | an integer in the type’s range | an integer from 0 to 255 |
float, double | a number, as double.parse reads it | a number |
io.Path | any text, as a path | |
| an enum whose cases carry no values | a case’s name in kebab case: DebugInfo is debug-info | 'fast' or 'debug-info' |
- An enum’s help lists its cases:
[possible values: fast, debug-info]. default_valueis written as on the command line, so the help shows it as it is:option<int>("jobs", default_value = "4"). A default that is not a value of the type panics.- Any other type is a compile error at the call.
Errors
parse writes a clang-style error, the usage and a hint to standard error
and returns false, so the program can exit with 2:
greet: error: unknown option '--lound'; did you mean '--loud'?
usage: greet [OPTIONS] [names]...
Run 'greet --help' for more information.
The messages:
unknown option '--x'(withdid you mean '--y'?for a near miss) andunknown option '-x'missing value for '-o'option '--loud' takes no value(for--loud=yes)invalid value 'x' for '--times': expected an integermissing required option '--target',missing required argument '<source>'unexpected argument 'x'when there is no positional argument left for itunknown command 'x'andmissing command: expected 'add' or 'update'
A mistake parse cannot see, such as two options that cannot be used
together, is reported the same way with usage_error, which returns 2:
if (test.is_set and library.is_set) {
return parser.usage_error("'--test' cannot be used with '--lib'");
}
try_parse does the same parse and writes nothing. It returns a
Result<String?>: an error holding the message, some text when --help or
--version asked for it (print it and exit with 0), or none when the
arguments were parsed. Tests use it, and so can a program that reports
errors its own way.
Declaring options as a struct
add_options declares an option or positional argument for each public
field of a struct, and every successful parse stores the values in that
struct’s fields. The attributes [cli.Option] and [cli.Positional] on
the fields carry what the builder calls take as arguments:
# Command-line parsing: declare the options as a struct's fields.
import cli;
struct Options {
[cli.Option("g", "what to say")]
var greeting: String = "hello";
[cli.Option("l", "shout it")]
var loud: bool;
[cli.Option("n", "how many times to say it", value_name = "COUNT")]
var times: int = 1;
[cli.Positional("who to greet")]
var names: String[];
}
main(args: String[]): int {
var options = Options();
var parser = cli.Parser("greet", "Greets someone", version = "1.0");
parser.add_options(options);
if (!parser.parse(args)) {
return 2;
}
var people = options.names.is_empty ? ["world"] : options.names;
for (var person in people) {
var line = $"{options.greeting}, {person}";
for (var _ in 0..options.times) {
print(options.loud ? line.upper() : line);
}
}
return 0;
}
Its help and errors are the same as the builder example’s above: the declarations are the same ones.
| Field type | Declares | After a parse |
|---|---|---|
bool | flag | whether it was given |
T | option<T> | the value given, else the field’s starting value |
T? | option<T> | the value given, or Empty |
T[] | multiple<T> | the values given, in order |
Tis any type of Typed values:String, a number type,io.Pathor an enum. A field’s starting value is the default the help shows, except an emptyStringor a nullio.Path, which means none.- An option is named after its field, with each
_written-:import_pathis--import-path. Any other field type is a compile error at the call:'add_options' cannot declare field 'table' of type 'Map<String, int>'; use bool, String, a number type, io.Path or an enum, or one of those as T? or T[]. [cli.Option(short_name, help, name, value_name, required, global)]sets the rest, every argument optional:[cli.Option("o", "the executable to write")],[cli.Option(required = true)].[cli.Positional(help, name, required)]makes the field the next positional argument. ATone is required when it has no default orrequiredis set; aT?one may be left out; aT[]one takes every value left (positionals), andrequiredasks for at least one.- Each parse sets every field again, so a flag left out is false even if the field started true.
- A parse that fails, or that asks for
--helpor--version, leaves the fields as they were. - The two styles mix: call
flagandoptionon the same parser, andadd_optionson a command’s parser for that command’s options. A command’s struct is filled only when that command is given.
Subcommands
command(name, about) declares a subcommand, as in git add or
pep-package update, and returns its own Parser to declare its options
and arguments on. The first argument that is not an option names the
command; after parse, is_selected tells which command was given.
# Subcommands, and handling the outcome yourself with try_parse.
import cli;
main(): int {
var parser = cli.Parser("pkg", "Manages packages");
var verbose = parser.flag("verbose", "v", "say more");
var add = parser.command("add", "Adds a dependency");
var name = add.positional("name", "the package to add");
var dev = add.flag("dev", "d", "only for development");
var update = parser.command("update", "Updates dependencies");
var packages = update.positionals("packages", "what to update; everything when none");
# The arguments as `main(args: String[])` would receive them.
var parsed = parser.try_parse(["pkg", "-v", "add", "--dev", "yaml"]);
assert(parsed.is_ok() && parsed.value().is_empty(), "parsed, no help asked for");
assert(add.is_selected && !update.is_selected, "the add command");
assert(verbose.is_set && dev.is_set && name.value() == "yaml", "its values");
# A mistake is an error with a clang-style message.
parsed = parser.try_parse(["pkg", "updat"]);
assert(parsed.error().message == "unknown command 'updat'; did you mean 'update'?", "a suggestion");
# --help gives the text to print, for the command it follows.
parsed = parser.try_parse(["pkg", "update", "--help"]);
print(parsed.value().value());
return 0;
}
- A parser with commands needs one, and cannot have positional arguments of its own.
- Options before the command’s name belong to the parent, and everything
after it to the command, so
pkg -v add xworks andpkg add -v xdoes not, unless-vis global. - A global option (
flag("verbose", "v", global = true), and the same argument onoptionandmultiple) may also be given after the name of any command below its parser, at any depth, and each command’s help lists it. A command cannot declare an option with a global option’s name, and a required global option may be given anywhere on the line. --helpafter a command shows that command’s help, and errors show its usage.- Commands nest: a command’s parser can declare commands too.
Shell completions
completions(shell) returns a script that completes the program’s
command line in Bash, Zsh, fish or PowerShell (cli.Shell), from the
declarations: options (with the global ones in each command), commands,
an enum’s cases, and file names for a value that is a String or an
io.Path. A program prints it on request, often from a command of its
own, as pkg completions fish does here:
# Shell completion scripts, printed by a command of the program's own.
import cli;
main(): int {
var parser = cli.Parser("pkg", "Manages packages");
parser.flag("verbose", "v", "say more", global = true);
var add = parser.command("add", "Adds a dependency");
add.positional("name", "the package");
var completions = parser.command("completions", "Prints a shell completion script");
var shell = completions.positional<cli.Shell>("shell", "bash, zsh, fish or powershell");
# The arguments as `main(args: String[])` would receive them.
var parsed = parser.try_parse(["pkg", "completions", "fish"]);
assert(parsed.is_ok() && completions.is_selected, "the completions command");
print(parser.completions(shell.value()));
return 0;
}
The user installs what it prints where the shell looks for it:
| Shell | Install the script as |
|---|---|
bash | ~/.local/share/bash-completion/completions/pkg, or source it from ~/.bashrc |
zsh | _pkg in a directory on $fpath |
fish | ~/.config/fish/completions/pkg.fish |
powershell | dot-sourced from $PROFILE |
Not yet
- Windows-style
/optionforms: every platform uses-and--.
Cryptographic hashes
The digest module computes SHA-256, SHA-1 and BLAKE3 digests: fixed-size
fingerprints of data, used to check that a download arrived intact, to name
content by its hash, or to key a cache. It also computes
message authentication codes with HMAC or
BLAKE3’s keyed mode, and derives keys. Bring it in with import digest;
the full API is in the reference.
# Cryptographic hashes: one-shot, incremental, and as hexadecimal text.
import digest;
main(): int {
# One call hashes a whole input, bytes or a String's UTF-8 bytes.
var id = digest.to_hex(digest.sha256("hello"));
assert(id == "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", "SHA-256");
# A hasher takes the input in pieces, such as the blocks of a file as
# they are read; the digest does not depend on how it was split.
var hasher = digest.Blake3();
hasher.update("hel");
hasher.update("lo");
var whole = digest.blake3("hello");
assert(hasher.finish() == whole, "same digest");
assert(whole.count == 32, "32 bytes");
# git names a file's content by the SHA-1 of a header and the bytes.
u8[] nul = [0];
var git = digest.Sha1();
git.update("blob 6");
git.update(nul);
git.update("hello\n");
assert(git.finish_hex() == "ce013625030ba8dba906f756967f9e9ca394464a", "git blob id");
return 0;
}
One-shot and incremental
Each hash has a one-shot function and a hasher type:
| Hash | Function | Hasher | Digest size |
|---|---|---|---|
| SHA-256 | digest.sha256(input) | digest.Sha256 | 32 bytes |
| SHA-1 | digest.sha1(input) | digest.Sha1 | 20 bytes |
| BLAKE3 | digest.blake3(input) | digest.Blake3 | 32 bytes |
- The input is a
u8[], or aString, which hashes its UTF-8 bytes (without a terminating NUL). - A hasher takes the input in pieces: call
updateany number of times, thenfinishfor the digest. Splitting the input differently gives the same digest.finishalso resets the hasher, so it can hash another input;resetdiscards the input so far. - A digest is a
u8[].digest.to_hex(bytes)writes it as lowercase hexadecimal, the way checksums are usually shown, and each hasher’sfinish_hex()does both steps;digest.from_hex(text)reads hexadecimal back into bytes. - An array literal passed straight to
updateis ambiguous between itsu8[]andStringoverloads; give it a type first (u8[] nul = [0]; hasher.update(nul);).
Which hash
- SHA-256 is the common choice for published checksums and anything that must interoperate.
- BLAKE3 is faster and as strong; the compiler’s build cache keys its objects with it.
- SHA-1 is broken for collision resistance: someone can make two inputs with the same digest. Use it only where a format requires it, such as git object ids.
None of these is the hash for a hash table: HashMap and HashSet use
hash<T>, which is much faster but neither cryptographic nor
stable between versions.
Message authentication
A message authentication code (MAC) is a digest that only someone with a secret key can compute, so a receiver that shares the key can check that a message is authentic and unchanged. Webhook signatures, JWTs (HS256) and S3-style request signing all use HMAC.
# Message authentication: HMAC, BLAKE3's keyed mode and key derivation.
import digest;
main(): int {
# A webhook signs its body with HMAC-SHA-256 under a shared secret;
# the receiver computes the MAC again and compares in constant time.
var body = "{\"event\":\"push\"}";
var received = digest.hmac_sha256("webhook secret", body);
var signature = "493db758cb288b3801a0084bcdd27bd565ca0fb9b0a149404d2b4442c9d79bcd";
assert(digest.to_hex(received) == signature, "HMAC-SHA-256");
var expected = digest.Hmac<digest.Sha256>("webhook secret");
expected.update(body);
assert(digest.equal_constant_time(expected.finish(), received), "authentic");
# Derive a 32-byte key for one purpose from a shared secret, then use
# it for BLAKE3's keyed mode.
var derive = digest.Blake3.derive_key("example.com 2026-10-16 webhook MACs v1");
derive.update("a shared secret");
var mac = digest.Blake3.keyed(derive.finish());
mac.update(body);
assert(mac.finish().count == 32, "a 32-byte MAC");
# BLAKE3 gives output of any length; the first 32 bytes are the digest.
var hasher = digest.Blake3();
hasher.update(body);
var long_output = hasher.finish(64);
assert(long_output.count == 64, "64 bytes");
assert(digest.to_hex(long_output).starts_with(digest.to_hex(digest.blake3(body))), "a prefix");
return 0;
}
- HMAC (RFC 2104) works over SHA-256 or SHA-1:
digest.hmac_sha256(key, message)anddigest.hmac_sha1(key, message)are one-shot, both argumentsu8[]or bothString, anddigest.Hmac<digest.Sha256>(key)is the incremental hasher, with the sameupdate,finish,finish_hexandresetas the hashes.finishandresetkeep the key. A key longer than 64 bytes is hashed first. - Check a MAC with
digest.equal_constant_time(a, b), not==:==stops at the first byte that differs, and timing it lets an attacker find a valid MAC a byte at a time. - BLAKE3’s keyed mode,
digest.Blake3.keyed(key), is a MAC with a 32-byte key; it panics on a key of any other length. - Key derivation:
digest.Blake3.derive_key(context)returns a hasher whose digest is a key, derived from the input (a shared secret, say) for the purposecontextnames. Makecontexta hard-coded string unique to the application and purpose; different contexts give unrelated keys. - Extended output: every
Blake3hasher’sfinish(length)returnslengthbytes, in any of the three modes. The first 32 are the digestfinish()returns.
Using the system’s crypto library
By default the hashes are written in Pepper, so a program that uses them
links no extra library. Building with -D USE_LIBCRYPTO (see
Build options) makes SHA-256 and SHA-1 call the
platform’s crypto library instead, which uses the CPU’s SHA instructions
where it has them: several times faster (about 1.7 GB/s instead of
250 MB/s for SHA-256 on an x86-64 with SHA-NI).
pep-build main.pep -D USE_LIBCRYPTO -o app
| Platform | Library | What it needs |
|---|---|---|
| Linux | OpenSSL’s libcrypto (EVP_Digest*), linked as -lcrypto | the development package (libssl-dev, openssl-devel) |
| macOS | OpenSSL’s libcrypto, linked as -lcrypto | Homebrew’s openssl@3, and its lib folder named with -L or PEPPER_LIBRARY_PATH (-L $(brew --prefix openssl@3)/lib) |
| Windows | CNG (BCryptHash*), linked as bcrypt.lib | nothing: CNG is part of Windows 10 and later |
The API and the digests are the same either way. Blake3 stays portable
Pepper, since libcrypto has no BLAKE3.
Limitations
- Without
USE_LIBCRYPTO, SHA-256 and SHA-1 run at a few hundred megabytes per second. BLAKE3 is always the portable code, below the official BLAKE3, which uses SIMD and threads. - The hashes have no secret-dependent branches or table lookups, but they have not been audited for timing side channels, which matter only when hashing secrets. Copies of an HMAC key stay in memory until the hasher is freed; nothing wipes them.
- HMAC works over SHA-256 and SHA-1; there is no SHA-512 to put under it.
Random numbers
The random module gives random numbers two ways: secure bytes from the
operating system, for keys, tokens and identifiers, and Generator, a fast
pseudo-random generator for everything else: games, simulations, sampling,
shuffling and jitter. Bring it in with import random; the full API is in
the reference.
# Random numbers: this thread's generator, a seeded one, and secure bytes.
import random;
main(): int {
# This thread's generator, seeded from the operating system.
var die = random.next_int(1..>6);
assert(die >= 1 && die <= 6, "a die roll");
var names = ["ada", "grace", "edsger"];
random.shuffle(names);
var pick = random.choose(names);
assert(names.contains(pick), "one of the names");
# A seeded generator gives the same numbers on every run and platform.
var a = random.Generator(2024);
var b = random.Generator(2024);
assert(a.next_double() == b.next_double(), "same seed, same numbers");
# Keys and tokens come from the operating system, not a generator.
var key = random.bytes(32);
assert(key.count == 32, "256 bits");
return 0;
}
Secure bytes
| Function | Gives |
|---|---|
random.bytes(count) | a new u8[] of count random bytes |
random.fill(bytes) | overwrites every element of an existing u8[] |
random.secure_ulong() | a random ulong |
They come from getentropy on Linux, Android, macOS, iOS and WASI, and
from BCryptGenRandom on Windows, so no one can predict them. Each call
asks the kernel, which is slower than a generator. They panic only if the
operating system cannot give random bytes, which a working system never
does.
Generators
random.Generator is xoshiro256**: 256 bits of state, statistically
strong output, and a few instructions a number. It is not secure: anyone
who sees enough of its output can predict the rest.
Generator()seeds itself from the operating system, so each run gives different numbers.Generator(seed)gives the same numbers for the sameseedon every platform, for reproducible tests and simulations.
| Method | Gives |
|---|---|
next_ulong() | 64 random bits |
next_int(range) | an int in range: 0..10 is 0 to 9, 1..>6 is 1 to 6 |
next_double() | a double from 0 up to but not including 1 |
next_bool() | true or false |
choose(items) | one element of an array |
shuffle(items) | puts an array in a random order, in place |
Every number in a range is equally likely: next_int draws again rather
than let the low numbers come up a little more often. An empty range, or
choose on an empty array, panics.
A generator is a value struct: copying one copies its state, so the copy gives the same numbers as the original, and a function that takes a generator as a parameter draws from its own copy. A generator belongs to one thread.
This thread’s generator
random.next_ulong, next_int, next_double, next_bool, choose and
shuffle draw from a generator each thread has of its own, seeded from the
operating system the first time the thread uses it. Code that needs a
random number calls them without passing a generator around, and threads
never share or lock one.
Encodings and UUIDs
Networked programs keep writing bytes as text and back: credentials in an HTTP header, a token in a URL, a value in a query string, an identifier in a database. These live in a few places:
| What | Where | Functions |
|---|---|---|
| Base64 | import base64 | base64.encode, base64.decode |
| Hexadecimal | import digest | digest.to_hex, digest.from_hex |
| Percent-escapes | Uri, in the prelude | Uri.encode, Uri.decode |
| Form data | Uri, in the prelude | Uri.encode_form, Uri.decode_form |
| UUIDs | import uuid | Uuid.v4, Uuid.v7, Uuid.parse, to_string |
# Encodings: base64, hexadecimal, percent-escapes and form data, and UUIDs.
import base64;
import digest;
import uuid;
main(): int {
# base64: an HTTP basic authentication header, and a URL-safe,
# unpadded token as JSON Web Tokens write them.
var credentials = base64.encode("ada:secret".bytes());
assert(credentials == "YWRhOnNlY3JldA==", "standard, padded");
var token = base64.encode([251, 255, 0], true, false);
assert(token == "-_8A", "URL-safe, unpadded");
assert(base64.decode(token, true).value()[0] == 251, "decoded");
assert(base64.decode("not base64!").is_err(), "rejected");
# Hexadecimal, both ways.
assert(digest.from_hex(digest.to_hex([1, 171])).value()[1] == 171, "round trip");
# Percent-escapes for one path segment or query value, and form data.
assert(Uri.encode("a b&c") == "a%20b%26c", "escaped");
var query = Uri.encode_form([("q", "pepper lang"), ("page", "2")]);
assert(query == "q=pepper+lang&page=2", "form-encoded");
var pairs = Uri.decode_form(query).value();
assert(pairs[0].1 == "pepper lang", "decoded");
# UUIDs: random (v4) and time-ordered (v7).
var id = uuid.Uuid.v4();
assert(uuid.Uuid.parse(id.to_string()).value() == id, "round trip");
assert(uuid.Uuid.v7().version() == 7, "time-ordered");
return 0;
}
Base64
base64.encode(bytes) writes RFC 4648 base64 with the standard alphabet
(A-Z a-z 0-9 + /) and = padding. Two optional arguments change that:
url_safe = true uses - and _ in place of + and /, so the text can
go in a URL or a file name, and padded = false leaves off the padding, as
JSON Web Tokens do.
base64.decode(text) returns a Result<u8[]>. It accepts text with or
without padding (pass url_safe = true for the other alphabet), and is
strict otherwise: whitespace, a character outside the alphabet, a length no
encoding gives, or unused bits that are not zero is an error. Remove line
breaks from PEM or MIME text before decoding it. The functions work on
whole arrays; there is no streaming encoder yet.
Hexadecimal
digest.to_hex(bytes) writes two lowercase digits per byte, and
digest.from_hex(text) reads them back, in either case; an odd length or
a character that is not a digit is an error.
Percent-escapes and form data
Uri.encode(text) escapes every byte outside RFC 3986’s unreserved set
(A-Z a-z 0-9 - . _ ~), so the result is safe as one path segment or one
query value: /, ?, &, = and % are escaped too. Uri.decode
reverses it.
Uri.encode_form(pairs) writes names and values as
application/x-www-form-urlencoded, the format of HTML forms and most
query strings (q=pepper+lang&page=2), and Uri.decode_form(text) reads
it back into an Array<(String, String)>, in order, keeping repeated
names. In this format a space is +.
UUIDs
uuid.Uuid is a 128-bit identifier (RFC 9562), a
value struct of two ulong halves, high and low,
so == and hash work and it can key a map.
Uuid.v4()makes a random one: 122 bits from the operating system’s secure source.Uuid.v7()puts the wall clock’s Unix time in milliseconds first, then 74 random bits, so ids made later sort later, both as values and as text. That keeps database indexes compact. Ids made in the same millisecond are in no particular order.Uuid.parse(text)reads the usual form,0190163d-8694-739b-aea5-966c26f8ad91, in either case, andto_string()(or interpolation) writes it in lowercase.version()returns the version digit andbytes()the 16 bytes, for a database or a binary format.
The full API is in the reference: base64,
uuid, hexadecimal
and Uri.
Optimizer hints
Hints tell the optimizer what it cannot see in the code. A hint never changes what a program computes, only how fast it runs, so a wrong hint costs speed and nothing else.
- Which way a branch goes: write
[Likely]or[Unlikely]above theiformatcharm (see branch hints). - A function rarely called:
[Cold]above it (see cold functions). - Memory a loop will soon touch: the
hintmodule’s prefetches.
| Function | Tells the optimizer |
|---|---|
hint.prefetch_read(address) | address will be read soon: start loading its cache line now. |
hint.prefetch_write(address) | address will be written soon. |
A prefetch never faults, even for an address that is not mapped, and does nothing where the CPU has no prefetch instruction.
# Prefetching: start loading memory a loop will soon read.
import hint;
main(): int {
var text = "a long text that a loop will read";
hint.prefetch_read(text.c_str());
var spaces = 0;
for (var index in 0..text.length) {
if (text.byte_at(index) == ' ') {
spaces += 1;
}
}
assert(spaces == 7, "a hint changes nothing the program computes");
return 0;
}
The prefetches are LLVM’s llvm.prefetch intrinsic (see
LLVM intrinsics). They matter only with
optimization (-O2 and up, where the runtime’s functions inline into the
caller); at -O0 they cost a call and do nothing.
Cold functions
[Cold] above a function, method or constructor marks it rarely called:
the optimizer never inlines it and treats a call to it as the unlikely
side of a branch. Split the rare work out of a hot function into a [Cold]
one, so the hot function stays small enough to inline into its callers:
# A hot method with its rare work split out into a [Cold] one.
struct Counter {
var count: int;
var wraps: int;
construct() {
count = 0;
wraps = 0;
}
# Called on every event: small enough to inline.
add() {
if (count == 1000) {
_wrap();
}
count += 1;
}
# Called once per thousand events: kept out of line.
[Cold]
_wrap() {
count = 0;
wraps += 1;
}
}
main(): int {
var counter = Counter();
for (var index in 0..2500) {
counter.add();
}
assert(counter.wraps == 2 && counter.count == 500, "[Cold] changes nothing the program computes");
return 0;
}
[Cold] is LLVM’s cold and noinline function attributes. The runtime’s
allocator marks its slow paths this way.
Deprecated hints
hint.likely(condition) and hint.unlikely(condition) are deprecated:
each use warns and suggests the attribute.
SIMD vectors
A SIMD vector holds several numbers of one type, its lanes, and computes
on all of them with one instruction. The simd module has two types:
Vector<T, N>:Nlanes of the number typeT.Mask<N>:Nbooleans, what a lane-wise comparison gives.
import simd;
# The index of the first byte of `text` that is not ASCII, or its size:
# sixteen bytes per step, then one at a time for the rest.
first_non_ascii(text: String): ulong {
var bytes = unsafe_cast<__ptr<byte>>(text.c_str());
ulong i = 0;
while (i + 16 <= text.size) {
var high = simd.Vector<byte, 16>.load(bytes + i).greater_equal(0x80 as byte).to_bits();
if (high != 0) {
return i + high.trailing_zeros();
}
i = i + 16;
}
while (i < text.size && bytes[i] < 0x80) {
i = i + 1;
}
return i;
}
main(): int {
# A vector holds `N` lanes and computes on all of them at once.
var a = simd.Vector<int, 4>(10);
a[1] = 20;
var b = a * 2 + 1;
assert(b[0] == 21 && b[1] == 41 && b.sum() == 104, "lane-wise arithmetic");
# A comparison gives a mask, one bool per lane.
var big = b.greater(30);
assert(big.to_bits() == 2 && big.any() && !big.all(), "a mask");
var capped = big.select(simd.Vector<int, 4>(30), b);
assert(capped.maximum() == 30, "select");
# Floats, and conversions between lane types.
var halves = a.convert<float>() * 0.5;
assert(halves[1] == 10.0 && halves.square_root()[0] < 2.3, "floats");
assert(first_non_ascii("plain ascii, then caf\u{e9}") == 21, "a scan");
assert(first_non_ascii("short") == 5, "no match");
return 0;
}
Bring them in with import simd; and write simd.Vector<byte, 16>, or
import [Vector, Mask] from simd; and write Vector<byte, 16>.
Lane types and counts
-
Tis an integer or floating-point type:byte,char,short,ushort,int,uint,long,ulong,floatordouble. These conform to the module’sLanetrait; any other type is an error at the type argument:main.pep:3:25: error: 'String' does not conform to trait 'Lane', required by 'T' in 'Vector' -
Nis written as a number, a power of two from 1 to 64:main.pep:3:30: error: lane count of 'Vector<int, 3>' must be a power of two from 1 to 64, not '3'A number is a type argument only here, for a lane count; it is not a general constant parameter, so code cannot compute with
N(use thecountproperty for the lane count as a value). -
Sixteen bytes (
Vector<byte, 16>,Vector<short, 8>,Vector<int, 4>,Vector<float, 4>,Vector<double, 2>) fits a vector register on every target Pepper supports and is the width to reach for first. A wider vector works everywhere: where the target’s registers are narrower, each operation takes several instructions.
A vector is a value struct: it is stored inline, copied
like a number, and safe to share between threads (a [Shared] struct’s
field, a module constant).
Operations
| Make | Vector<T, N>() (every lane 0), Vector<T, N>(x) (every lane x) |
| Lanes | v[i], v[i] = x (panics when i >= N), v.count |
| Arithmetic | + - *, unary -, with a vector or a scalar: v * 2 |
| Division | / %: integer lanes panic on a zero divisor lane, and on the lowest value divided by -1; float lanes follow IEEE |
| Bitwise (integers) | & | ^ ~, << >> by an int count (panics outside 0.. the lane width; >> is arithmetic for signed T), rotate_left, rotate_right |
| Floats | square_root, floor, ceiling |
| Both | min, max, abs |
| Horizontal | sum, minimum, maximum |
| Comparisons | equal, not_equal, less, less_equal, greater, greater_equal, with a vector or a scalar, giving a Mask<N> |
| Whole vector | == (every lane equal), != (some lane differs) |
| Rearranging | shuffle(indices), reverse, interleave_low, interleave_high |
| Types | convert<U>() (lane by lane, like as), reinterpret<U, M>() (the same bits) |
| Memory | Vector<T, N>.load(array, index), v.store(array, index) (panic unless index + N <= array.count); load, store, load_aligned, store_aligned on a __ptr<T> |
A mask has any(), all(), & | ^ ~, m[i], select(a, b) (each lane
from a where the mask is true, else from b) and to_bits(), which packs
lane i into bit i of a ulong. With trailing_zeros() (see
integer bit methods) that finds the first true
lane, which is how a scan for a byte, or for a byte that is not ASCII, finds
its match sixteen bytes at a time.
Some differences from scalar arithmetic are deliberate:
-
Integer lanes wrap around on overflow in
+,-,*, unary-andabs, as vector instructions do; scalar arithmetic panics instead. -
Comparisons that give a lane per lane are methods, because a comparison operator must return
bool(see operators).==and!=compare whole vectors. -
shuffletakes each index moduloN, so every index picks a lane. With constant indices an optimized build makes it one shuffle instruction. -
convertfrom float to integer saturates: a lane outside the target type’s range becomes its nearest value, and NaN becomes 0. -
The bitwise operators, shifts and rotates exist only for integer lanes, and
square_root,floorandceilingonly for float lanes:main.pep:4:15: error: no member named 'square_root' in 'Vector<int, 4>'
Targets and optimization
The operations compile to LLVM vector instructions, which the target turns
into its own: SSE2 on x86-64 (wider with -march=x86-64-v3 for AVX2, see
pep-build), NEON on arm64, and WebAssembly’s
simd128 with -msimd128. A target without vector instructions, such as
wasm32-wasip1 without -msimd128, computes the same results one lane at a
time. Nothing picks instructions at run time: the -m options decide.
Each operation is a small method that the compiler always inlines, so an
optimized build (-O2) of a loop over vectors has no calls in it, only the
vector instructions. At -O0 it still runs, more slowly, like the rest of
an unoptimized program.
Logging
The log module writes lines with a level, a message, key/value fields and
the place of the call, from any thread or task. Bring it in with
import log.
# Leveled, structured logging: levels, fields, a target, and a JSON file.
import io;
import log;
main(): int {
log.info("started");
log.warning("disk almost full", [log.field("free_mb", 120)], target = "storage");
log.debug("hidden: the default level is info");
log.set_level(log.Level.Debug, "storage");
if (log.enabled(log.Level.Debug, "storage")) {
log.debug("scanning", [log.field("path", "/var/data")], target = "storage");
}
# From here on, lines go to a file as JSON Lines.
var path = io.Directory.temp().join("logging-basics.log");
var file = io.File.create(copy(path)).value();
log.set_output(move(file), log.Format.Json);
log.error("cannot connect", [log.field("attempt", 3)], target = "db");
log.set_output(io.File.stderr());
var text = io.File.read_to_string(path).value();
assert(text.starts_with("{\"time\":"), "one JSON object per line");
assert(text.contains("\"level\":\"error\",\"target\":\"db\",\"message\":\"cannot connect\""), "the line's keys");
assert(text.contains("\"attempt\":3}"), "the field, as JSON");
io.File.remove(path);
return 0;
}
On standard error this prints:
2026-10-17T23:48:53.555Z INFO started (logging-basics.pep:6:5)
2026-10-17T23:48:53.555Z WARN [storage] disk almost full free_mb=120 (logging-basics.pep:7:5)
2026-10-17T23:48:53.555Z DEBUG [storage] scanning path="/var/data" (logging-basics.pep:12:9)
| Function | Does |
|---|---|
log.error(message), warning, info, debug, trace | Writes a line at that Level. |
log.field(key, value) | Makes a Field for a line’s fields; the value is written as JSON. |
log.set_level(level, target) | Sets the least important level shown, for one target or (with no target) for all. |
log.enabled(level, target) | Whether a line at level for target would be written. |
log.set_output(writer, format) | Sends lines to a Writer in Format.Text or Format.Json. |
Lines
Each logging function takes the message, then two optional arguments, usually named:
fields: an array oflog.field(key, value). A text line shows each askey=value, with the value’s JSON text (200,"/var/data",true).target: the part of the program the line is about, such as"http". A text line shows it in brackets, and levels can be set for it alone.
Each line names the place of the call, as assert does. The time is UTC,
to the millisecond. On a terminal the level is in color,
unless the NO_COLOR environment variable is set (on Windows, only in a
console already in virtual terminal mode, such as Windows Terminal).
Levels
The levels, most important first, are Error, Warning, Info, Debug
and Trace. A target shows its level and every level above it; Off
shows nothing. By default every target shows Info.
The PEPPER_LOG environment variable sets the levels when the program
first logs: comma-separated items, each a level for every target or
target=level for one. The names are off, error, warning (or
warn), info, debug and trace, in any case. An item with an unknown
level is reported on standard error and skipped.
$ PEPPER_LOG=warning,http=debug ./server
log.set_level changes them from code. A target named in PEPPER_LOG or
set_level uses its own level; every other target uses the global one.
Cost
A line no target shows costs two atomic loads: no lock and no formatting.
The arguments are still evaluated, so a message or field that is costly to
build goes inside an if (log.enabled(...)), as in the example.
Output and threads
Lines go to standard error as text until log.set_output names a
Writer, such as a file or a socket, and a format:
Format.Text: the readable lines above.Format.Json: one JSON object per line (JSON Lines) withtime,level,target(when there is one),messageandlocation, then each field.
The log keeps the writer, so it must be a new value or one handed over
with move(x). A File keeps its path, so make it from copy(path), as
the example does.
Each line is built first and then written whole, with one write_all,
under a Mutex, so lines from many threads and
tasks never interleave. A task waiting for that lock parks instead of
blocking its worker thread. A failed write is ignored.
Not yet
- Levels cannot be removed at compile time.
- The runtime’s own modules (such as
http) and thepep-*tools do not log throughlogyet.
Backtraces and the debugger
The debug module walks the call stack and talks to a debugger. Bring it in
with import debug.
| Function | Does |
|---|---|
debug.capture() | Records the call stack as a Backtrace: the return address of each call, innermost first. |
trace.frames() | Resolves the addresses to StackFrames: function, file, line, column, address and inlined. |
debug.print_backtrace() | Writes the current stack to standard error, as a panic does. |
debug.output(text) | Writes to the platform’s debugger channel. |
debug.is_debugger_attached() | Whether a debugger is attached. |
debug.breakpoint() | Stops in the debugger, as a breakpoint on that line would. |
# Capturing the call stack and resolving it to functions and lines.
import debug;
struct Parser {
var depth: int;
construct() {
depth = 0;
}
# Records where it was called from, as an error report might.
where(): debug.Backtrace {
return debug.capture();
}
}
main(): int {
var parser = Parser();
var trace = parser.where();
var frames = trace.frames();
for (var frame in frames) {
print(frame.to_string());
}
# The outermost frame is main. (With -O and no -g, `where` is inlined
# into main and has no frame of its own; WebAssembly cannot walk its
# stack, so there a backtrace is empty.)
if (frames.count > 0) {
assert(frames[frames.count - 1].function == "main", "the outermost call");
}
debug.output("done");
return 0;
}
Built with -g, this prints:
Parser.where at debug-backtrace.pep:13:16
main at debug-backtrace.pep:19:24
done
Built with -g -O2, the optimizer inlines where into main, and the
backtrace still shows it, marked as inlined:
Parser.where at debug-backtrace.pep:13:16 (inlined)
main at debug-backtrace.pep:19:24
done
Capturing is cheap; resolving is not
capture() only records addresses, so a program can capture a stack
whenever it might want to report one later, such as when it creates an error
value. The work happens in frames() and to_string(), which read the
program’s symbol table and line table each time they are called.
A function name needs the program’s symbol table, which every build keeps
unless the executable is stripped. A file, line and column need the debug
info a -g build carries:
| Platform | Function names | Source lines |
|---|---|---|
| Linux | The executable’s ELF symbol table | Its DWARF line table |
| macOS | The executable’s symbol table | The .dSYM bundle a -g build writes beside the executable (with dsymutil, from Xcode’s command line tools) |
| Windows | DbgHelp, from the .pdb every build writes beside the executable | DbgHelp, from the .pdb of a -g build (no columns) |
| WASI | none | none |
A WebAssembly program cannot walk its own stack, so on WASI a backtrace is empty.
Inlined calls
A function the optimizer inlined into its caller has no frame of its own on
the stack. In a -g build, frames() puts it back: it expands each address
into the chain of inlined calls it is inside, as addr2line -i and
debuggers do, with one StackFrame per call, innermost first. Each of these
frames has inlined set and is at the line of its own call, and the frame
after them, which is not inlined, is the real one. All of them share its
address. Linux and macOS read the inlined calls from the DWARF debug info,
and Windows asks DbgHelp. On Windows an inlined call to a generic function
is named without its type arguments (helper, not helper<int>), since
the .pdb records inlined calls that way.
Without -g there is no record of what was inlined, so an optimized build’s
backtrace shows only the real frames, each named after the function the
code was inlined into.
Which frames are left out
frames() leaves out, at the top of the stack, the frames of capture
itself and of the runtime’s panic code, and the runtime’s operator methods a
panic came through: a panic in a[i] starts at the line with a[i], not in
Array<int>.__index_get. It also leaves out the C runtime’s frames below
main. frames(full = true) keeps them all.
Backtraces on panic
A panic prints the stack after its
message in a build without -O:
main.pep:3:9: panic: index out of range
backtrace:
0: inner at main.pep:3:9
1: main at main.pep:8:12
An optimized build prints the message alone. The PEPPER_BACKTRACE
environment variable overrides the build: PEPPER_BACKTRACE=1 prints a
backtrace in any build, PEPPER_BACKTRACE=full also prints the frames
frames() leaves out, and PEPPER_BACKTRACE=0 never prints one. Without
-g, each frame shows its function but no file or line.
Only the first panic in a process prints a backtrace. If a second thread panics at the same time, it prints just its message.
The debugger channel
debug.output(text) writes a line where a debugger shows it:
- Windows:
OutputDebugStringW. Visual Studio’s Output window and WinDbg show it, and so does DebugView when no debugger is attached. It goes nowhere else. - Linux, macOS and WASI: standard error. These systems have no separate channel, and debuggers show the program’s own output.
debug.is_debugger_attached() reads TracerPid in /proc/self/status on
Linux, P_TRACED on macOS, and IsDebuggerPresent on Windows. It is always
false on WASI.
debug.breakpoint() executes the processor’s debug trap. A debugger stops
there and can continue the program. With no debugger attached, the trap
ends the program, so guard it:
if (debug.is_debugger_attached()) {
debug.breakpoint();
}
break is a keyword, so the function is called breakpoint.
See Debugging for running a program under gdb, lldb or Visual Studio.
Unit tests
A unit test is a function marked [Test]: it takes no parameters, returns
nothing, and passes when it returns. It checks what the code under test
does with expect_equal and the other expect_* functions, which are in
the prelude, so a test file needs no import:
# Counts the words in `text`: the runs of characters between spaces.
word_count(text: String): ulong {
ulong count = 0;
for (var word in text.split(" ")) {
if (!word.is_empty) {
count += 1;
}
}
return count;
}
[Test]
counts_the_words() {
expect_equal(word_count("salt and pepper"), 3);
expect_equal(word_count(" spaced out "), 2);
}
[Test]
an_empty_string_has_no_words() {
expect_equal(word_count(""), 0);
expect_true(word_count(" ") == 0);
}
[Test]
[ShouldPanic("out of bounds")]
reading_past_the_last_word_panics() {
var words = "salt pepper".split(" ");
var third = words[2];
}
# A normal build leaves the tests out and runs this.
main(): int {
print(word_count("salt and pepper"));
return 0;
}
pepper test (the pep-test tool) builds the
tests and runs them:
$ pepper test words_test.pep
running 3 tests
test counts_the_words ... ok (4 ms)
test an_empty_string_has_no_words ... ok (3 ms)
test reading_past_the_last_word_panics ... ok (5 ms)
3 passed, 0 failed
Given a folder, or nothing (the current folder), it runs every test in the
.pep files under it. Its exit status is 0 only when every test passes.
Checks
Each check panics when it fails, as assert does, and reports what it
compared at the line of the call, so a failing test says where and why:
words_test.pep:14:5: panic: expect_equal failed
actual: 2
expected: 3
| Check | Fails when |
|---|---|
expect_equal(actual, expected) | actual == expected is false; prints both |
expect_not_equal(actual, unexpected) | the two are equal; prints the value |
expect_true(condition) | condition is false |
expect_false(condition) | condition is true |
expect_equal and expect_not_equal take two values of one type that has
== and a text form (see to_string in String
interpolation). assert and panic fail a test too:
any panic does.
Tests that should panic
[ShouldPanic] turns a test around: it passes only when it panics. With a
message, [ShouldPanic("out of bounds")], the panic’s message must also
contain that text, so a test does not pass on some other panic. A
[ShouldPanic] test that returns fails.
How tests run
Each test runs in a child process of its own, so a panic, a crash or a
test that never ends fails only that test, and the others carry on. Tests
run in parallel, one per logical CPU (--jobs 1 runs them one at a time,
in order), and a test still running after 60 seconds fails (--timeout
changes the limit). Tests share no state: each process starts fresh.
A test also fails when it leaks: the test program counts the live allocations before and after the test, and a test that returns with more (or fewer) than it started with fails, saying how many it left behind:
error: test 'counts_the_words' left 1 allocation behind
What a program makes once and keeps until it ends, such as a global made
on first use, is not a leak. A test that is first to use it wraps that
use in keep_for_the_program, which runs it and leaves what it allocates
out of the count:
keep_for_the_program(() => {
tls.TlsConnector.create(); # loads the TLS library, which stays loaded
});
There is no setup or teardown: a fixture is a plain function that a test
calls, and what it makes is released when the test’s scope ends, as
everywhere else. A struct whose destructor cleans up makes a fixture that
tears itself down. TempFolder, which every test file can use, is one: a
fresh folder under the system’s temporary folder, removed with what is in
it when the test returns:
import io;
[Test]
writes_a_file() {
var folder = TempFolder("writes_a_file");
var path = folder.path.join("notes.txt");
io.File.write_text(path, "hello");
expect_equal(io.File.read_to_string(path).value(), "hello");
}
Where tests live
A test can sit beside the code it tests, where it can call the file’s
private functions, as above. Or tests can live in their own files, such as
a module’s tests/ folder, where they import the module and see its public
API; pepper test -I <dir> adds the folder that holds the module to the
import path:
geometry/
module.pep
shapes.pep
tests/
area_test.pep # import geometry; [Test] functions
pepper test -I . geometry/tests
pep-test looks at each .pep file under the folders it is given and
builds those with a line that starts with [Test]. The test files of one
folder build into one test program, whose tests are their [Test]
functions. Folders whose names start with . are skipped.
Each file keeps the scope it has in any program. A folder inside a module
(a test/ folder below its module.pep, say) holds files of that module,
which see each other’s names: two of them that declare the same helper,
or the same test name, collide (redefinition of 'helper'). Mark such a
declaration
private in each file,
or rename it. Test files outside a module see only each other’s exports,
so their names never collide.
Test builds
A normal build leaves [Test] functions out, so they cost nothing, and a
file of tests can keep a main of its own. pep-build --test, which
pep-test uses, builds the test program instead:
- it keeps the
[Test]functions and defines the build optionTEST, so code only the tests use (a fixture that would be unused otherwise) can be marked[BuildOption("TEST")]; - it replaces the entry file’s
mainwith one that answers--list(each test, one per line) and--run NAME(run one test), which is howpep-testdrives it. Given several files of one folder, it builds the tests of them all into one program, and--run FILE::NAMEtells apart two tests of one name.
A [Test] function that takes parameters, returns a value or is generic is
an error, as are [Test] on a method and [ShouldPanic] without [Test]:
main.pep:2:8: error: test 'checks' must take no parameters
2 | checks(x: int) {}
| ^
The language server checks a file that declares a [Test] function as a
test build does: with the build option TEST and testing loaded, so
errors, hover and go-to-definition work inside test bodies with no setting
(see pep-lsp).
Tests that compare a program’s printed output with an expected file are not supported yet.
Calling C
Pepper programs can call functions written in C: the C standard library, your
operating system’s API, or any other library with a C interface. You declare
the C function’s prototype with extern, make sure the library that defines it
is linked, and then call it like any Pepper function.
Declaring a C function
An extern declaration is a C prototype, written the way C writes it: the
return type, the name, and the parameter types, each followed by a parameter
name. It has no body.
extern ulong strlen(char* s);
extern int strcmp(char* a, char* b);
extern void* malloc(ulong size);
extern void free(void* p);
After that, strlen("hello") calls the C library’s strlen. The compiler
does not check your prototype against any C header, so it must match the real
C function exactly. A wrong parameter or return type compiles, and then passes
or reads the wrong bits when the program runs.
Match C types with Pepper types of the same size:
| C | Pepper |
|---|---|
char, signed char, unsigned char | char or i8, u8 or byte |
short, unsigned short | short or i16, ushort or u16 |
int, unsigned int | int or i32, uint or u32 |
long long, unsigned long long, int64_t, size_t (64-bit) | long or i64, ulong or u64 |
float, double | float, double |
T* | T* in the prototype |
Pepper’s long is always 64 bits. C’s long is 64 bits on Linux and macOS but
32 bits on Windows, so a C function that takes long needs a different
prototype per platform (see [Link(name = ...)]
below for an example).
32-bit targets
On a 32-bit target (wasm32-wasip1, see
WebAssembly), pointers, C’s long and
size_t are 32 bits. There, an extern’s long and ulong stand for C’s
pointer-sized long/ssize_t and unsigned long/size_t: the call passes
the low 32 bits and widens the result back to 64 (sign-extending a long,
zero-extending a ulong), so extern ulong strlen(char* s); works
unchanged. A Pepper function passed to C as a callback is converted the same
way in the other direction. i64 and u64 stay 64 bits on every target; use them for C’s
long long, int64_t and off_t. WebAssembly checks every call’s
signature, so a prototype with the wrong width traps instead of returning
garbage.
Some things to know about extern declarations:
- The prototype uses C syntax only. The
name(parameter: type): typeform that Pepper functions use is a parse error on an extern, and__ptr<T>as a parameter type is an error too: writechar*, not__ptr<char>. - An extern is private to its file (or module), like every declaration. Write
export extern ...to let files that import this one call it (see Visibility). - Several files may declare the same extern, since every declaration names the
same C function. The runtime already declares
malloc,freeand a few others privately, and your program can declare them again. - An extern is linked by name. If no linked library defines it, the program compiles and then fails to link, with the linker reporting an undefined symbol. See Linking for how to add libraries.
- An extern is a function value too, of the function
type its prototype gives (
absis an(int) => int), so it can be stored and passed like a Pepper function. Where a C function pointer type is expected, it is the C function’s own address (see Passing a Pepper function to C). A variadic extern (one ending in..., below) is not a value: wrap a call to it in a closure.
# An extern function is a function value, like a Pepper function.
extern int abs(int x);
extern ulong strlen(char* s);
apply(f: (int) => int, x: int): int {
return f(x);
}
main(): int {
# Its type comes from the prototype: `abs` is an `(int) => int`.
var absolute = abs;
assert(absolute(-3) == 3, "through a local");
assert(apply(abs, -4) == 4, "as an argument");
# A `char*` parameter is a `__ptr<char>` in the function type.
var length: (__ptr<char>) => ulong = strlen;
var word = "four";
assert(length(word.c_str()) == 4, "strlen");
# Where a C function pointer is expected, it is the C function itself.
var pointer: extern (int) => int = abs;
assert(pointer(-5) == 5, "C function pointer");
return 0;
}
Passing strings
Pepper’s String is a reference-counted object, not
a C string, so a String value cannot be passed where C expects char*.
- A string literal passed to an extern is passed as a NUL-terminated C
string, with no allocation:
strlen("hello")is 5. The same is true wherever a literal flows into a__ptr<char>,__ptr<u8>, or__ptr<void>in Pepper code (a variable, field, argument, or return value), soString("hello")copies the literal’s five bytes. - A
StringConstantpasses its constant NUL-terminated bytes with no copy, by itself where a__ptr<char>is expected or throughc_str(). The pointer is valid for the whole run of the program and is never__null. - A
Stringvalue is passed withc_str(), which returns the String’s buffer, NUL-terminated, as a__ptr<char>. A String is not guaranteed to be NUL-terminated otherwise, so always go throughc_str()when C needs the terminator. The pointer belongs to the String: it is valid until the String is destroyed or grows (for example withappendor+=). An emptyString()has no buffer, so itsc_str()is__null; check before you pass it to a C function that does not acceptNULL. - To turn a C string back into a
String, pass the pointer to the constructor:String(p)copies a NUL-terminated C string, andString(p, size)copiessizebytes. If you already know the number of code points, pass it as a third argument:String(p, size, String.length_of(p, size)). The constructor copies the bytes, so you still own (and may free) the C buffer. A C string is never passed or stored where aStringis expected without this copy: that is an error.
# Passing strings to C, and turning C strings back into Strings.
extern ulong strlen(char* s);
extern int strcmp(char* a, char* b);
extern char* strchr(char* s, int c);
main(): int {
# A string literal passed to an extern is a NUL-terminated C string.
assert(strlen("hello") == 5, "a literal is passed as a C string");
assert(strcmp("abc", "abc") == 0, "equal C strings compare as 0");
# So is one stored in a __ptr<char>, or passed to the String
# constructor, which copies it.
__ptr<char> greeting = "hello";
assert(strlen(greeting) == 5, "a literal in a __ptr<char> is a C string");
assert(String("hello").size == 5, "String(literal) copies its bytes");
# A String value goes through c_str().
var name = "Pepper";
name += " and C";
assert(strlen(name.c_str()) == name.size, "c_str() is the String's bytes");
# A char* result is a __ptr<char>. Copy it into a String with the
# constructor: the byte size and the number of code points.
var rest = strchr(name.c_str(), 32); # 32 is ' '
assert(rest != __null, "strchr found a space");
var size = strlen(rest);
var copy = String(rest, size, String.length_of(rest, size));
assert(copy == " and C", "the copy holds the tail of the C string");
return 0;
}
Passing a String value directly to an extern’s char* parameter is an error:
main.pep:4:19: error: cannot pass 'String' to parameter 1 of extern 'strlen' of type '__ptr<char>'; pass its 'c_str()'
4 | return strlen(name) as int;
| ^~~~
The reverse is an error too: a char* (or any raw pointer, void*
included) is never a String, so passing one where a String is expected,
assigning it to a String variable, or returning it as one is rejected. Copy
it into a String with String(p) first:
main.pep:3:19: error: cannot pass a value of type '__ptr<char>' as argument 2 of 'assert', which has type 'String'; use 'String(...)' to copy a C string
Variadic functions
A C function with a variable argument list, such as printf or snprintf,
ends its prototype with ... after at least one named parameter:
extern int snprintf(char* buffer, ulong size, char* format, ...);
The call uses the C calling convention for variadic functions, which differs
from a plain call on some targets (such as macOS on Apple silicon). Each
argument past the named ones gets C’s default argument promotions: integers
narrower than int widen to int, and float widens to double. Match your
format string to the promoted type: %d for a u8, %f for a float, and
%lld (not %ld) for a long, because C’s long is 32 bits on Windows.
... must come last and must follow a named parameter: extern int f(...);
is a parse error.
# Calling a C function that takes a variable argument list.
extern void* malloc(ulong size);
extern void free(void* p);
# On Windows, snprintf lives in legacy_stdio_definitions.
[Target(OS(Windows))] [Link("legacy_stdio_definitions")]
extern int snprintf(char* buffer, ulong size, char* format, ...);
[Target(!OS(Windows))]
extern int snprintf(char* buffer, ulong size, char* format, ...);
main(): int {
var buffer = malloc(64) as __ptr<char>;
u8 small = 200; # widened to int
float half = 0.5; # widened to double
long big = 5000000000;
var written = snprintf(buffer, 64, "%d %.1f %lld %s", small, half, big, "ok");
var text = String(buffer, written as ulong, String.length_of(buffer, written as ulong));
free(buffer);
assert(text == "200 0.5 5000000000 ok", "snprintf formatted every argument");
return 0;
}
On Windows, the C runtime defines snprintf and its relatives inline in its
headers, so the linkable symbols live in a separate library,
legacy_stdio_definitions. The example links it with [Link] on Windows only.
Pointers
C code deals in raw pointers, and Pepper has a raw pointer type for talking to
it: __ptr<T>, such as __ptr<void>, __ptr<char> or __ptr<__ptr<int>>.
A void* returned by an extern is a __ptr<void> in Pepper, and a
__ptr<T> can be passed to an extern that takes a C pointer.
A pointer supports these operations:
| Operation | Meaning |
|---|---|
p[i] | reads or writes the element i places from p; its type is T, so var v = p[i] declares a T |
p + n | the address n elements after p (n bytes for __ptr<void>) |
p == __null, p != __null | compares with the null pointer |
p == q, p != q | compares two addresses (a pointer also compares with an integer address) |
p as __ptr<void>, v as __ptr<T> | the same address, to and from __ptr<void> |
unsafe_cast<__ptr<U>>(p) | the same address, seen as pointing at U |
unsafe_cast<ulong>(p), unsafe_cast<__ptr<T>>(n) | the address as an integer, and back |
as converts a pointer only to and from __ptr<void>, like static_cast in
C++. Any other pointer conversion reinterprets memory, so it is spelled
unsafe_cast (see Reinterpreting with
unsafe_cast).
__null is the null pointer. sizeof<T>() gives the size of a type in bytes,
which you need to allocate memory for C. The
compiler built-ins reference lists these and the
other __ intrinsics.
T* spelling is for extern prototypes only, since they mirror C. Everywhere
else (parameters, return types, struct fields, locals, globals, type aliases,
generic arguments and casts) you write __ptr<T>, and T* is a compile error:
clear(p: void*, n: ulong) { }
main.pep:1:10: error: C pointer syntax `void*` is only allowed in extern declarations; use `__ptr<void>`
1 | clear(p: void*, n: ulong) { }
| ^~~~
# Raw pointers: __ptr<T> in Pepper code, T* in extern prototypes.
extern void* malloc(ulong size);
extern void free(void* p);
extern void* memset(void* p, int value, ulong size);
struct Buffer {
var data: __ptr<u8> = __null;
var size: ulong = 0;
}
make_buffer(size: ulong): Buffer {
var b = Buffer();
b.data = malloc(size) as __ptr<u8>;
b.size = size;
memset(b.data, 0, size);
return b;
}
sum(values: __ptr<int>, count: int): int {
var total = 0;
var i = 0;
while (i < count) {
total = total + values[i];
i = i + 1;
}
return total;
}
# `var` takes the element type from the pointer: `big` is a ulong, so a value
# above 2^32 reads back whole.
first(values: __ptr<ulong>): ulong {
var big = values[0];
return big;
}
main(): int {
# Indexing reads and writes elements of T.
var numbers = malloc(4 * sizeof<int>()) as __ptr<int>;
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
numbers[3] = 40;
assert(sum(numbers, 4) == 100, "indexing reads what was written");
var wide = malloc(sizeof<ulong>()) as __ptr<ulong>;
wide[0] = 5000000000;
assert(first(wide) == 5000000000, "var v = p[i] has the pointee's type");
free(wide);
# p + n moves by n elements, not n bytes.
var third = numbers + 2;
assert(third[0] == 30, "numbers + 2 points at the third element");
assert(unsafe_cast<ulong>(third) - unsafe_cast<ulong>(numbers) == 8, "two ints are eight bytes");
# __ptr<void> strides by bytes; cast to see the memory another way.
var raw = numbers as __ptr<void>;
var bytes = raw as __ptr<u8>;
assert(bytes[4] == 20, "the second int starts at byte 4 (little-endian)");
free(raw);
# Pointers can live in struct fields, and compare with __null.
var b = make_buffer(16);
assert(b.data != __null, "malloc returned memory");
assert(b.data[15] == 0, "memset cleared it");
free(b.data);
b.data = __null;
# A pointer to pointers: indexing yields the inner pointer, and indexing
# again reads or writes through it.
var rows = malloc(2 * sizeof<__ptr<int>>()) as __ptr<__ptr<int>>;
var row = malloc(sizeof<int>()) as __ptr<int>;
rows[1] = row;
rows[1][0] = 7;
assert(rows[1][0] == 7, "rows[1][0] reads through the row pointer");
assert(row[0] == 7, "rows[1] is the row pointer");
free(row);
free(rows);
return 0;
}
Pointers are not reference-counted and nothing checks them: indexing past the
end of an allocation, using memory after free, or forgetting to free it
behaves exactly as it would in C. Keep raw pointers inside small functions or
structs that own the memory, and give the rest of your program ordinary Pepper
types.
Owning a C resource: Handle<T>
A C library hands out objects that it frees with a function of its own
(fclose, free, LLVMDisposeModule). Pepper’s reference counting does
not know about them, so the runtime’s Handle<T> ties one to Pepper’s
lifetimes, like C#’s SafeHandle: it holds the raw value and a release
function, and calls the function once, when the last reference to the
handle goes away.
# A C resource owned by a Handle: `free` runs when the last reference to
# the handle goes away.
extern void* malloc(ulong size);
extern void free(void* p);
extern void* memset(void* p, int value, ulong size);
# A buffer from C, freed with the struct that holds it.
struct Buffer {
var _memory: Handle<__ptr<void>>;
var size: ulong { get; }
construct(size: ulong) {
_memory = Handle<__ptr<void>>(malloc(size), (p) => free(p));
this.size = size;
}
fill(value: u8) {
memset(_memory.value, value as int, size);
}
at(index: ulong): u8 {
return (_memory.value as __ptr<u8>)[index];
}
}
main(): int {
var buffer = Buffer(16);
buffer.fill(7);
if (buffer.at(15) != 7) {
return 1;
}
# release() frees early; the handle then no longer owns the memory.
var scratch = Handle<__ptr<void>>(malloc(8), (p) => free(p));
scratch.release();
return scratch.is_owned() ? 2 : 0;
}
Handle<T>(value, release)takes ownership ofvalue.Tis usually__ptr<void>or an integer descriptor.valuereads the raw value, to pass to C.- A handle is a reference, so copies share the resource; it is released
when the last copy goes away. Keep it in a field of the struct that
wraps the resource, as
Bufferdoes. release()frees the resource early, anddetach()gives it back without freeing it. Either way the release function never runs again, andis_owned()becomes false.releaseis a function value, so a closure may capture what the release needs. A captured reference stays alive until the release runs, which is how a C object that must not outlive its parent (an LLVM module and its context) keeps the parent alive.
Passing a Pepper function to C
A Pepper function value is a reference-counted object,
not a C function pointer. For C, Pepper has a separate type,
extern (P...) => R: the address of a function that C calls with the C
calling convention. Declare a callback parameter of a C function with it,
and pass a named function, or a closure that captures nothing:
# `extern (P...) => R` is a C function pointer type: qsort calls `compare`
# with pointers to two elements.
extern void qsort(void* base, ulong count, ulong size, extern (void*, void*) => int compare);
ascending(a: __ptr<void>, b: __ptr<void>): int {
__ptr<int> x = a;
__ptr<int> y = b;
return x[0] - y[0];
}
main(): int {
__ptr<int> numbers = _pep_malloc(4 * sizeof<int>());
numbers[0] = 30;
numbers[1] = 10;
numbers[2] = 40;
numbers[3] = 20;
# A named function converts to the C function pointer type.
qsort(numbers, 4, sizeof<int>(), ascending);
assert(numbers[0] == 10 && numbers[3] == 40, "ascending");
# So does a closure that captures nothing; its parameter types come
# from the C function pointer type.
qsort(numbers, 4, sizeof<int>(), (a, b) => {
__ptr<int> x = a;
__ptr<int> y = b;
return y[0] - x[0];
});
assert(numbers[0] == 40 && numbers[3] == 10, "descending");
# A C function pointer is a value like a number: store it, call it.
var compare: extern (__ptr<void>, __ptr<void>) => int = ascending;
assert(compare(numbers, numbers + 1) > 0, "40 sorts after 30");
_pep_free(numbers);
return 0;
}
-
What converts. A named free function, an
externfunction, a static method (Order.descending), or a closure literal that captures nothing, whose parameter and return types match the C function pointer type exactly. A closure literal takes its parameter types from the type, as it does from any function type. -
Nothing captured. C calls the function with its arguments alone, so a closure that captures a value, or a bound method (
p.get), cannot convert:main.pep:4:43: error: closure passed as C function pointer type 'extern (__ptr<void>, __ptr<void>) => int' captures 'bias'; a C function pointer cannot hold captured values 4 | qsort(_pep_malloc(4), 0, 4, (a, b) => bias); | ^~~~ main.pep:3:9: note: pass what the callback needs through the C function's context argument instead 3 | var bias = 1; | ^~~~This is a design decision, not a missing feature: a C function pointer is a bare address, and giving each capturing closure an address of its own would mean writing executable code at run time, which some platforms forbid. A callback that needs state gets it another way (see Callbacks that need state).
A function value held in a variable does not convert either, since it may be a closure.
-
C types only. C knows nothing of reference counts, so the parameter and return types must be numbers,
bool, pointers, or other C function pointer types. A struct is an error:C function pointer type 'extern (String) => int' cannot use 'String'; C passes only numbers, 'bool' and pointers. Write pointers asvoid*in anexterndeclaration and as__ptr<void>elsewhere, as for any pointer. -
A value like a number. A C function pointer has no reference count. Store it in a variable, field or module-scope
const, return it, pass it to other threads, or call it:compare(a, b)calls through it with the C calling convention. Calling a null one (from C) panics. It converts tovoid*but not to a Pepper function type; wrap it in a closure that calls it,(x) => f(x), to use it as one.
__function_pointer(name) also gives a function’s address, as an untyped
__ptr<void>. It takes a free function, or a static method of the struct
whose method uses it. Inside a generic struct the static method is the one
for the current type arguments, which is how Thread.spawn gives the
operating system its thread entry point. Prefer a C function pointer type
where the signature is known, since the compiler then checks it.
Callbacks that need state
Since a C function pointer holds no captured values, a callback that needs state gets it another way:
- The C function’s context argument. Most C APIs that take a callback
also take a
void*context argument that they hand back to it; pass a pointer to the data the callback needs through that. - A thread-local or module-scope value. Set a
[ThreadLocal]variable, anasync.ThreadLocal<T>or anAtomic<T>before calling C, and read it in the callback, which is a named function:
# A C function pointer holds no captured values, so a callback that needs
# state reads it from somewhere C does not have to carry: here, a
# thread-local the caller sets before handing the callback to C.
extern void qsort(void* base, ulong count, ulong size, extern (void*, void*) => int compare);
# The value to sort around, for the calling thread.
[ThreadLocal]
var pivot: int = 0;
distance(x: int): int {
var d = x - pivot;
return d < 0 ? -d : d;
}
closest_first(a: __ptr<void>, b: __ptr<void>): int {
__ptr<int> x = a;
__ptr<int> y = b;
return distance(x[0]) - distance(y[0]);
}
main(): int {
__ptr<int> numbers = _pep_malloc(4 * sizeof<int>());
numbers[0] = 10;
numbers[1] = 40;
numbers[2] = 22;
numbers[3] = 31;
pivot = 30;
qsort(numbers, 4, sizeof<int>(), closest_first);
assert(numbers[0] == 31 && numbers[3] == 10, "closest to 30 first");
_pep_free(numbers);
return 0;
}
Binding to a different symbol name
[Link(name = "symbol")] binds an extern to a C symbol whose name differs
from the declared one. Pepper code calls the extern by its declared name, and
the object file refers to symbol. Use it to give a C function a clearer
name, to avoid clashing with a Pepper name, or, together with
[Target], to bind one Pepper name to a different
symbol on each platform. lib names the library that defines the symbol in
the same attribute, as [Link("library")]
does on its own:
# [Link(name = "symbol")] binds a Pepper name to a C symbol with a different name.
# llabs takes a 64-bit long long on every platform: `i64` in an extern is
# 64 bits everywhere, where `long` follows the size of a pointer.
[Link(name = "llabs")]
extern i64 absolute(i64 value);
# C's long is 64 bits on Linux and macOS but 32 bits on Windows and
# WebAssembly, so the 64-bit labs is only right on the first two.
[Target(OS(Windows) || OS(Wasi))]
[Link(name = "llabs")]
extern i64 abs64(i64 value);
[Target(OS(Linux) || OS(Mac))]
[Link(name = "labs")]
extern long abs64(long value);
# `lib` names the library that defines the symbol, as [Link("m")] does.
[Target(OS(Linux))]
[Link(lib = "m", name = "cbrt")]
extern double cube_root(double value);
main(): int {
assert(absolute(-7) == 7, "absolute calls llabs");
assert(abs64(-5000000000) == 5000000000, "abs64 calls a 64-bit abs");
[Target(OS(Linux))]
{
var root = cube_root(27.0);
assert(root > 2.999 && root < 3.001, "cube_root calls cbrt from libm");
}
return 0;
}
Real uses of the per-platform form include macOS on Intel, where the
64-bit-inode version of stat is named stat$INODE64:
[Target(OS(Mac) && Arch(x64))]
[Link(name = "stat$INODE64")]
extern int stat(char* path, void* buf);
[Target(OS(Mac) && Arch(aarch64))]
extern int stat(char* path, void* buf);
[Link] takes the library as its one positional argument or as lib, and
the symbol as name; each is a non-empty string, and an extern names at
most one symbol. Anything else is an error, such as:
main.pep:1:7: error: 'Link' has no argument named 'symbol'; use 'lib', 'path' or 'name'
1 | [Link(symbol = "labs")] extern long pep_abs(long v);
| ^~~~~~
The earlier [LinkName("symbol")] was removed; it is an error that suggests
the [Link(name = "symbol")] that replaces it.
LLVM intrinsics
An extern whose [Link(name = ...)] symbol starts with llvm. is not a C
function but an LLVM intrinsic:
a call to it compiles to the operation itself, usually one instruction, and
nothing is linked. The runtime’s bit and math methods
(Bit operations)
are written this way; declare one yourself for an intrinsic they do not
cover, such as a CPU-specific one:
# An extern bound to an LLVM intrinsic: a call is the instruction itself.
[Link(name = "llvm.umin")]
extern ulong smaller(ulong a, ulong b);
# `llvm.ctlz` takes a second argument that must be a constant.
[Link(name = "llvm.ctlz")]
extern ushort leading_zeros(ushort value, bool is_zero_poison);
main(): int {
assert(smaller(7, 3) == 3, "llvm.umin.i64");
assert(leading_zeros(1, false) == 15, "llvm.ctlz.i16");
return 0;
}
-
The extern’s prototype is the intrinsic’s signature, in the types Pepper maps to LLVM’s:
byteandchararei8,ushortandshorti16,uintandinti32,ulongandlongi64,booli1,floatanddoublethemselves, any pointerptr, and avoidreturn isvoid. Intrinsics that take or return vectors or structs cannot be declared yet. -
Name an overloaded intrinsic by its base name (
llvm.ctpop): the overload is picked from the prototype (llvm.ctpop.i32for auint). The mangled name works too. -
A prototype that matches no overload is an error at the extern, with LLVM’s reason:
main.pep:2:13: error: 'popcount' does not match the signature of LLVM intrinsic 'llvm.ctpop': intrinsic has incorrect argument type 2 | extern long popcount(int value); | ^~~~~~~~ -
Some arguments must be constants, such as
llvm.ctlz’sis_zero_poison; passing a variable is an error at the argument:main.pep:5:19: error: argument 2 to 'leading_zeros' must be a constant 5 | return leading_zeros(1, zero_poison); | ^~~~~~~~~~~
An unknown name (llvm.nonsense) is an error too. A target-specific
intrinsic (llvm.x86.*, llvm.aarch64.*) only exists on its architecture,
so pair it with [Target(Arch(...))].
Linking the library
A C function is only callable if the library that defines it is linked into
your program. The C library is always linked. For anything else, attach
[Link("name")] to the extern or pass -l name to the compiler. The
Linking chapter covers both.
[Target(OS(Windows))]
[Link("gdi32")]
extern ulong GetStockObject(int kind);
Calling Pepper from C
C code can call Pepper functions: mark each one [Export], and build the
program as a library with --lib static or --lib shared. The compiler
writes a C header beside the library that declares every exported function.
# A library C code calls: build it with `pepper build counter.pep --lib
# static` (or `--lib shared`), which also writes `counter.h`.
#
# `main` is not needed in a library; this one calls the exported functions
# from Pepper, so the example runs as a program too.
[Export]
add(a: int, b: int): int {
return a + b;
}
# A struct crosses to C as an opaque handle, `Counter*`.
struct Counter {
var count: int;
construct(this.count = 0) {}
}
# The C name can differ from the Pepper name.
[Export(name = "counter_new")]
make_counter(start: int): Counter {
return Counter(start);
}
[Export]
counter_increment(counter: Counter): int {
counter.count += 1;
return counter.count;
}
# A C function pointer, called back.
[Export]
apply(f: extern (int) => int, x: int): int {
return f(x);
}
# A String arrives as UTF-8 bytes and their size, and leaves as a
# NUL-terminated copy that C frees with `pepper_string_free`.
[Export]
greet(name: String): String {
return "hello, " + name;
}
main(): int {
var counter = make_counter(41);
assert(counter_increment(counter) == 42, "the counter counts");
assert(add(2, 3) == 5, "add adds");
assert(greet("C") == "hello, C", "greet greets");
return 0;
}
pepper build counter.pep --lib static # libcounter.a and counter.h
pepper build counter.pep --lib shared # libcounter.so and counter.h
The header declares the functions with C types:
typedef struct Counter Counter;
void counter_retain(Counter* handle);
void counter_release(Counter* handle);
void pepper_string_free(char* text);
int32_t add(int32_t a, int32_t b);
int32_t apply(int32_t (*f)(int32_t), int32_t x);
int32_t counter_increment(Counter* counter);
Counter* counter_new(int32_t start);
char* greet(const char* name, size_t name_size);
and a C program calls them like any C function:
#include <stdio.h>
#include "counter.h"
static int32_t twice(int32_t x) { return 2 * x; }
int main(void) {
Counter* counter = counter_new(41);
printf("%d\n", counter_increment(counter)); /* 42 */
counter_release(counter);
printf("%d\n", apply(twice, add(2, 3))); /* 10 */
char* greeting = greet("C", 1);
printf("%s\n", greeting); /* hello, C */
pepper_string_free(greeting);
return 0;
}
cc main.c -I. -L. -lcounter -lm -o main # Linux; macOS needs no -lm
Exporting a function
[Export]goes on a free function that is not generic. Its C name is its Pepper name;[Export(name = "c_name")]gives it another, which must be a C identifier. Two exports with one C name are an error.[Export]anywhere else (a struct, a method, an extern, a variable, a generic function) is a warning, and does nothing.- An exported function is still a Pepper function: Pepper code calls it by its Pepper name.
- Do not confuse
[Export]with theexportkeyword, which makes a declaration visible to other Pepper files (see Visibility). A function can be either, both or neither.
What crosses to C
Each parameter and the result must be a type C can hold:
| Pepper | C (in the header) |
|---|---|
bool | bool |
char, i8, u8, short, ushort, int, uint, long, ulong (and their i16… spellings) | char, int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t |
float, double | float, double |
__ptr<T> | T* for a number type, else void* |
extern (P...) => R | a C function pointer, R (*name)(P...) |
String | a parameter: const char* name, size_t name_size; a result: char* (see Strings) |
| a struct | an opaque handle, Name* |
Anything else is an error at the parameter or result that C cannot hold:
main.pep:2:15: error: cannot export 'total': C cannot hold 'Array<int>'
2 | total(values: Array<int>): int {
| ^~~~~~~~~~
note: only a non-generic struct passes to C, as a handle
A generic struct (Array<int>), a value struct, an enum, a trait, a tuple,
Optional and a Pepper function type cannot cross. Pass an array as a
pointer to its elements and a count, or wrap it in a struct of your own and
pass a handle.
Strings
A String crosses as UTF-8 bytes, copied each way, so neither side keeps a
pointer into the other’s memory:
- A
Stringparameter is two C parameters: a pointer to the bytes and their size,const char* name, size_t name_size. The bytes need no NUL terminator, and may contain NULs. The function gets its own copy for the call; C keeps its buffer. A size of 0 (with any pointer,NULLincluded) is the empty string. - A
Stringresult is a NUL-terminated copy of its bytes, aschar*, which the C code owns and frees withpepper_string_free, never withfree. An emptyStringis"", neverNULL. A string with NULs inside ends at its first one for C; return a pointer and a size instead when that matters. - The header declares
pepper_string_freewhen some export returns aString.
Handles
A struct reaches C as a pointer to an incomplete C type (Counter*), which
C code passes back but never looks inside. Handles follow Pepper’s
reference counting:
- A handle a function returns is one reference, which the C code owns.
It releases it with
<name>_release(counter_release) when it is done; the last release frees the object, running its destructor. - A handle C passes in stays C’s: the function borrows it for the call.
<name>_retainadds a reference, for C code that keeps a handle in two places; each retain needs a release.- The names are the struct’s name in snake case:
ByteBuffergetsbyte_buffer_retainandbyte_buffer_release. The header declares them for every struct an export passes.
A handle must not be null, and is freed at most once: C gets no checks on either.
Why a string is freed but a handle is released
The two look alike from C, a pointer Pepper hands over, but they are different things, and the names say which:
- A handle is a live Pepper object. Pepper may hold references to it
too: a field, a global, another thread. C holds one or more of those
references, so it uses the same counting Pepper does:
<name>_retainadds one,<name>_releasedrops one, and the object is destroyed only when the last reference anywhere goes away. Releasing is not freeing. - A returned string is not a Pepper object. It is a plain copy of the
bytes, made at the boundary like
strdupmakes one, with no count and no other owner. Pepper keeps no reference to it, so there is nothing to retain or share; the C code owns it outright and gives it back once withpepper_string_free. - Why not
free: the copy comes from Pepper’s allocator, which tracks live allocations, serves short strings from its own small block pools (see Where objects live) and, on Windows, may use a different C runtime’s heap than the caller’s.pepper_string_freereturns it to the allocator that made it.
A String parameter needs neither: Pepper copies the bytes for the call,
and C’s buffer stays C’s.
Building the library
--lib static writes an archive of the program’s objects: libname.a
(name.lib on Windows). --lib shared links a shared library:
libname.so, libname.dylib on macOS, or name.dll on Windows, with its
import library name.lib. -o chooses another name. Either way the header
is written beside the library, named for it without the lib prefix
(counter.h).
The compiler writes the archive itself, in the format of the platform it
builds for (with --target, another one’s: GNU’s for Linux and Android,
the BSD format Apple’s linkers read for macOS and iOS, the COFF archive for
Windows), with the index of the symbols its objects define, so no archiver
needs to be installed and a static library for another platform needs no
system root. A shared library links like an executable, with clang. With
--lto=full the library is optimized as one module first, as an
executable is (see --lto in pep-build).
- A library needs no
main. One that has amainkeeps it, and C code that links the library cannot have its own. - Only the exported functions and their handle functions are visible outside a shared library; everything else in it is hidden.
- A program that links the static library must also link the C libraries
Pepper’s runtime uses. The header lists them in a comment at the top, such
as
/* Link the static library with: -lm */on Linux. A shared library links them itself. - On Windows,
--lib staticand--lib sharedboth writename.lib(the archive, or the DLL’s import library), so build them into different folders. - A C program links at most one Pepper static library: each holds its own copy of the runtime, and two would define it twice. Shared libraries keep their runtimes to themselves, so a program may load several.
Initialization, panics and threads
- Module-scope variables are initialized when the library is loaded, before C calls any exported function (a static library’s initializer runs with the program’s other static constructors).
- A panic in Pepper code called from C prints its message and aborts the process, as it does in a Pepper program: it never unwinds into C.
- Exported functions may be called from any thread, including threads C
created. The thread-safety rules of threads still hold:
a handle to a struct that is not
[Shared]must stay on one thread at a time, and C does not check that.
Platform selection
Some code only makes sense on one platform: a Windows API call, a
Linux-specific constant, a type whose size differs between operating systems.
The [Target(...)] attribute keeps a declaration or a block only when you
compile for a matching platform, so one source file can hold every platform’s
version side by side.
The target is the platform you compile on, unless you name another with
--target (see Cross-compiling below).
Writing a condition
Target takes one boolean condition built from these predicates:
OS(...)is true on the named operating system:Windows,Mac(orMacOS),Linux,Wasi(WebAssembly, see below),IOSorAndroid(see iOS and Android). As in C, whereTARGET_OS_MACholds on every Apple OS and__linux__on Android,OS(Mac)is also true on iOS andOS(Linux)on Android: they share the kernel and most of the C library, so code for macOS or Linux usually works there unchanged. WriteOS(Mac) && !OS(IOS)for macOS alone, andOS(Linux) && !OS(Android)for desktop and server Linux alone.Arch(...)is true on the named processor architecture:x64,aarch64, orwasm32.BuildOption(NAME)is true when the build definesNAMEwith-D, and can be compared with a literal (BuildOption(LOG_LEVEL) >= 2); see Build options.
Values are matched case-insensitively, so OS(windows) works too. Combine
predicates with && (or and), || (or or), !, and parentheses:
[Target(OS(Windows))] # Windows
[Target(!OS(Windows))] # Linux and macOS
[Target(OS(Linux) && Arch(x64))] # Linux on x64 only
[Target(OS(Windows) || OS(Mac))] # Windows and macOS
[Target(!(OS(Windows) || OS(Mac)))] # Linux
An item may carry several attributes. They are combined with “and”: the item
is kept only when every Target on it matches. Other attributes on the same
item, such as [Link(...)], do not affect the selection.
What it applies to
Put [Target(...)] before any top-level declaration: a function, an extern,
a struct, an enum, a trait, an extension, a type alias, a module-scope
variable, an import, or a re-export. It also works on the methods inside a
struct, trait or extension, and on an enum’s cases. When two declarations share a name, give them conditions that
never match on the same platform, so exactly one survives:
# One name, a different declaration per platform.
# Type aliases: a Windows HANDLE is a pointer, a POSIX descriptor an int.
[Target(OS(Windows))] type Handle = __ptr<void>;
[Target(!OS(Windows))] type Handle = int;
# Module-scope variables (const and thread-safe, like every global).
[Target(OS(Windows))] const max_path = 260;
[Target(!OS(Windows))] const max_path = 4096;
# Functions.
[Target(OS(Windows))]
platform_name(): String { return "Windows"; }
[Target(OS(Mac))]
platform_name(): String { return "macOS"; }
[Target(OS(Linux))]
platform_name(): String { return "Linux"; }
# Structs, and methods inside them.
[Target(Arch(x64))]
struct Cpu {
var name: String = "x64";
}
[Target(Arch(aarch64))]
struct Cpu {
var name: String = "aarch64";
}
struct Word {
[Target(Arch(x64) || Arch(aarch64))]
bits(): int { return 64; }
}
main(): int {
var name = platform_name();
print(name);
print(Cpu().name);
assert(Word().bits() == 64, "every supported target is 64-bit");
[Target(OS(Windows))] {
assert(sizeof<Handle>() == 8, "a HANDLE is a pointer");
assert(name == "Windows", "the Windows function was kept");
}
[Target(!OS(Windows))] {
assert(sizeof<Handle>() == 4, "a descriptor is an int");
assert(max_path == 4096, "the POSIX variable was kept");
}
return 0;
}
The attribute goes before export, not after it:
[Target(OS(Windows))] export f(). Writing export [Target(OS(Windows))] f()
is a parse error.
Blocks inside a function
Inside a function body, attach [Target(...)] to a block { ... }. The block
is kept or removed like a declaration. Blocks may nest, and may appear inside
if and loop bodies.
This is the usual way to wrap C functions that differ per platform: declare
each platform’s extern with its own condition, and call each one from a
block with the same condition.
# A portable wrapper over a C function that has a different name on
# each platform. kernel32 is linked by default on Windows, and getpid is
# in the C library everywhere else.
[Target(OS(Windows))] extern uint GetCurrentProcessId();
[Target(!OS(Windows))] extern int getpid();
process_id(): long {
[Target(OS(Windows))] {
return GetCurrentProcessId() as long;
}
[Target(!OS(Windows))] {
return getpid() as long;
}
}
main(): int {
var id = process_id();
print($"process {id}");
assert(id > 0, "a running process has a positive id");
return 0;
}
What happens to code that does not match
Code whose condition does not match is removed before type checking. That is
what makes the examples above work: a call to GetCurrentProcessId, which
exists only on Windows, is fine in a block that is removed everywhere else.
Removed code is not checked for errors beyond parsing, so a typo in it goes
unnoticed until you build on the platform that keeps it. The VS Code
extension shows it dimmed, from its attribute to its end, with a hint saying
it is not compiled for the platform you are building on. A file that is
mostly code for another OS (a Linux-only file opened on Windows) is still
checked as that OS’s build would check it, so hover and go-to-definition
work in it. In such a file, an error only your own platform’s build would
report (portable code calling a function only the other OS has) appears
when you build, not in the editor.
It still has to parse, and every Target condition is validated on every
platform, even one inside removed code. A misspelled value is always an error,
never a silent “no match”:
[Target(OS(Darwin))]
f(): int { return 1; }
main.pep:1:1: error: unknown OS value `Darwin` (expected Windows, Mac, MacOS, Linux, Wasi, IOS, Android) in [Target(OS(Darwin))]
1 | [Target(OS(Darwin))]
| ^~~~~~~~~~~~~~~~~~~~
Other mistakes the compiler reports:
Targettakes exactly one condition:[Target(OS(Windows), Arch(x64))]is an error; write[Target(OS(Windows) && Arch(x64))].- Only
OS(...),Arch(...)andBuildOption(...)may appear inside it. A bare name ([Target(Windows)]), another call ([Target(Foo(x))]),notin place of!, or a nestedTargetis an error. OS(...)andArch(...)are only valid insideTarget(...), and the brackets hold one attribute:[OS(Windows)]and[Target(OS(Windows)) && Export]are parse errors.- Struct fields cannot carry
[Target(...)]. Select the whole struct instead, asCpudoes in the first example.
Cross-compiling
--target <triple> compiles for another platform: [Target(...)] selects
that platform’s code, and the compiler emits that platform’s machine code
and object format. The supported targets are each OS on each architecture:
| Triple | OS | Arch |
|---|---|---|
x86_64-unknown-linux-gnu | Linux | x64 |
aarch64-unknown-linux-gnu | Linux | aarch64 |
x86_64-apple-darwin | macOS | x64 |
aarch64-apple-darwin | macOS | aarch64 |
x86_64-pc-windows-msvc | Windows | x64 |
aarch64-pc-windows-msvc | Windows | aarch64 |
wasm32-wasip1 | Wasi | wasm32 |
aarch64-apple-ios | iOS | aarch64 |
aarch64-apple-ios-simulator | iOS (simulator) | aarch64 |
x86_64-apple-ios-simulator | iOS (simulator) | x64 |
aarch64-linux-android | Android | aarch64 |
x86_64-linux-android | Android | x64 |
clang’s other spellings work too: arm64 for aarch64, amd64 for
x86_64, any vendor, and an OS version (arm64-apple-macosx14.0,
aarch64-apple-ios17.0, aarch64-linux-android33). Any other triple is an
error that lists these. wasm32-wasi, wasm32-unknown-wasi and
wasm32-unknown-wasip1 all mean wasm32-wasip1. The iOS and Android
targets, their versions and their SDKs are described in
iOS and Android.
pepper build --target aarch64-unknown-linux-gnu main.pep -o main
Linking an executable for another platform needs that platform’s system
libraries and LLVM’s linker, lld: the compiler looks for it beside clang,
in its LLVM install and on the PATH, also under the versioned name
Ubuntu’s lld-20 package uses (ld.lld-20). Name
the directory that holds the libraries (the sysroot) with --sysroot <dir>,
or set PEPPER_SYSROOT_<TRIPLE>, the triple in capitals with _ for each
- (PEPPER_SYSROOT_AARCH64_APPLE_DARWIN):
-
Linux: a directory with the target’s glibc and GCC runtime (
/usr/aarch64-linux-gnufrom Debian’s or Ubuntu’slibc6-dev-arm64-crossandlibgcc-13-dev-arm64-cross, which clang also finds without--sysroot). From another OS, copy a Linux machine’s at the same paths: the C startup files andlibc,libm,libgcc_sand the dynamic loader from/usr/lib/x86_64-linux-gnu, andcrtbegin*.o,crtend*.oandlibgcc*from/usr/lib/gcc/x86_64-linux-gnu/<version>. -
macOS: a macOS SDK (
MacOSX.sdk,xcrun --show-sdk-pathon a Mac). Linking reads only itsSDKSettings.jsonand the library stubs inusr/lib. -
Windows: a directory laid out like a Visual Studio install, with the MSVC libraries and the Windows SDK, as xwin writes it:
xwin --accept-license --arch x86_64 splat --use-winsysroot-style --preserve-ms-arch-notation --output ~/winsysroot
CI links each of these from the other two (from Linux x64, macOS arm64 and
Windows x64, for x86_64-unknown-linux-gnu, aarch64-apple-darwin and
x86_64-pc-windows-msvc) and runs the programs on their own OS; its scripts,
scripts/ci-export-sysroot.sh and scripts/ci-provision-cross.sh, show the
setup end to end.
When the link fails, the error ends with a note naming what is missing:
note: linking for 'x86_64-pc-windows-msvc' needs that platform's system libraries and LLVM's linker (lld): pass '--sysroot <dir>' or set PEPPER_SYSROOT_X86_64_PC_WINDOWS_MSVC
The -m options are read for the target: -mcpu is an AArch64 option
and -march=x86-64-v3 an x86 one, and native, which names the CPU of the
machine you compile on, is an error for a target of another architecture.
The compiler only builds for the target; copy the executable to a machine of that platform to run it.
WebAssembly
--target wasm32-wasip1 compiles to a WebAssembly module for
WASI preview 1, which runtimes such as
wasmtime run from the command line:
pep-build --target wasm32-wasip1 --sysroot /opt/wasi-sdk main.pep -o main.wasm
wasmtime run --dir . main.wasm
Linking needs a wasi-sdk (its
sysroot holds wasi-libc and clang’s builtins) and LLVM’s wasm-ld, which the
compiler finds as it finds lld (also as wasm-ld-20). Name the SDK
directory, or its share/wasi-sysroot, with --sysroot, or set
PEPPER_SYSROOT_WASM32_WASIP1 or WASI_SDK_PATH. wasi-sdk 25 is the version
tested. The program needs a runtime’s --dir to reach a directory and
-S inherit-env (wasmtime) to see the environment. Directory.temp() is
TMPDIR, else /tmp, so grant one (--dir /tmp) to use it.
WASI preview 1 is a small system, and wasm32 is a 32-bit target, so some
things work differently:
OS(Wasi)code selects the WASI versions of the runtime’s platform layer. Code written for!OS(Windows)also compiles for WASI, so check that it only calls what wasi-libc has.- There are no threads and no stack switching:
Thread.spawnpanics, and so does starting tasks (async.run,async.run_with). Mutexes, channels and atomics still work, as long as nothing waits for another thread. - Sockets, TLS, HTTP connections and starting processes return an
IoErrorsaying they are not supported on WASI. - Files work through the directories the runtime grants.
Metadatahas no permission bits (read_onlyis false) and no creation time, andset_permissionsandset_read_onlyreturn an error;set_executableand themodeofOpenOptionsandDirectory.createdo nothing. Timestamps and symlinks work. osreportsOperatingSystem.Wasi,"wasm32", an empty host name and version, and no total memory.- Pointers and C’s
longandsize_tare 32 bits; see 32-bit targets.
iOS and Android
Pepper compiles natively for iOS and Android through the same LLVM backend
it uses everywhere else: --target picks the platform, and the result is
ordinary machine code for an iPhone, an iPad, the iOS simulator, or an
Android phone. An app calls the Pepper code through C, as any C library is
called: mark the functions [Export], build a library with --lib static
(see Calling Pepper from C), and call
it from Swift directly, or from Kotlin and Java through JNI.
Targets
| Triple | Runs on |
|---|---|
aarch64-apple-ios | iPhone and iPad |
aarch64-apple-ios-simulator | the iOS simulator on an Apple silicon Mac |
x86_64-apple-ios-simulator | the iOS simulator on an Intel Mac |
aarch64-linux-android | Android phones and tablets (arm64-v8a) |
x86_64-linux-android | the Android emulator on x64 (x86_64) |
-
Versions. A triple can name the oldest OS version the code runs on:
aarch64-apple-ios17.0,aarch64-apple-ios17.0-simulator,aarch64-linux-android33. Without one, iOS code runs on iOS 15 and later and Android code on API level 30 (Android 11) and later. Pepper’s runtime needs API level 30, so an older one is an error:error: unsupported target 'aarch64-linux-android24': Pepper's runtime needs Android API level 30 or later (write 'android30') -
Spellings.
arm64works foraarch64, as do Rust’s-simfor-simulatorand any vendor (aarch64-unknown-linux-android).x86_64-apple-iosmeans the simulator, since no iPhone is x64. -
CPU. Code for an iPhone runs on every arm64 iPhone (clang’s
apple-a7); add-mcpu=apple-a14to use what newer ones have. Android x64 code uses SSE4.2 and POPCNT, which the Android x86_64 ABI requires.
Building
A library for an app needs no SDK, only the compiler:
pepper build --target aarch64-apple-ios --lib static greeter.pep -o ios/libgreeter.a
pepper build --target aarch64-linux-android --lib static greeter.pep -o android/libgreeter.a
The compiler writes each archive itself, in the format the platform’s
linkers read (BSD’s for iOS, GNU’s for Android), so nothing from Xcode or
the NDK is needed to make it. Each writes greeter.h beside the library.
Build the library once for each
target the app ships: the device and the simulator on iOS, and each
Android ABI.
Linking, for a shared library or an executable to run in the simulator or an emulator, needs the platform’s SDK:
- iOS links with LLVM’s
ld64.lldagainst an iOS SDK. On a Mac with Xcode the compiler asksxcrunfor it (iphoneos, oriphonesimulatorfor the simulator). Elsewhere name it with--sysroot, or setPEPPER_SYSROOT_AARCH64_APPLE_IOS(andPEPPER_SYSROOT_AARCH64_APPLE_IOS_SIMULATOR, and so on): the variable is named for the triple without its version. - Android links with the NDK’s own clang, which brings the NDK’s
linker, compiler-rt and unwinder. Name the NDK with
--sysroot <ndk>orPEPPER_SYSROOT_AARCH64_LINUX_ANDROID, or setANDROID_NDK_HOMEorANDROID_NDK_ROOT; otherwise the newest NDK in the Android SDK ($ANDROID_HOME/ndk/<version>, as Android Studio installs it) is used. NDK r27 is the version tested.
When the SDK is missing, the link error says where to point the compiler:
note: linking for 'aarch64-linux-android30' needs the Android NDK (https://developer.android.com/ndk/downloads): pass '--sysroot <ndk>' or set PEPPER_SYSROOT_AARCH64_LINUX_ANDROID, ANDROID_NDK_HOME or ANDROID_HOME
Selecting code for a platform
[Target(OS(IOS))] and [Target(OS(Android))] select code for one of
them. Most code needs neither: iOS is Darwin, like macOS, and Android runs
the Linux kernel, so OS(Mac) is true on iOS and OS(Linux) on Android,
as TARGET_OS_MAC and __linux__ are in C (see
Platform selection). The
runtime’s macOS and Linux code serves them, with a few declarations
narrowed where they differ. At run time, os.system_os() returns
OperatingSystem.IOS or OperatingSystem.Android.
Calling Pepper from Swift
Swift calls C directly. Give swiftc the generated header as the bridging
header, and link the library:
xcrun --sdk iphonesimulator swiftc -target arm64-apple-ios15.0-simulator -parse-as-library \
-import-objc-header greeter.h App.swift -L . -lgreeter -o App.app/App
In an Xcode project, add the library to the target’s Link Binary With
Libraries phase and #include "greeter.h" in the target’s bridging header.
A String crosses as a pointer and a size, and a returned string is freed
with pepper_string_free:
func pepperGreeting(_ name: String) -> String {
let result = name.withCString { greet($0, strlen($0)) }!
defer { pepper_string_free(result) }
return String(cString: result)
}
Calling Pepper from Kotlin or Java
Kotlin and Java reach C through JNI: a shared library holds the Pepper code
and a C function for each native method, named for its class, which
converts Java’s types to the C ones the header declares. Link both with the
NDK’s clang into the .so the app loads:
pepper build --target aarch64-linux-android --lib static greeter.pep -o pepper/libgreeter.a
$NDK/toolchains/llvm/prebuilt/*/bin/clang --target=aarch64-linux-android30 -shared -fPIC \
-I pepper jni.c pepper/libgreeter.a -lm -o jniLibs/arm64-v8a/libgreeter.so
JNIEXPORT jstring JNICALL
Java_dev_pepper_demo_MainActivity_greet(JNIEnv* env, jclass cls, jstring name) {
const char* bytes = (*env)->GetStringUTFChars(env, name, NULL);
char* result = greet(bytes, strlen(bytes));
(*env)->ReleaseStringUTFChars(env, name, bytes);
jstring greeting = (*env)->NewStringUTF(env, result);
pepper_string_free(result);
return greeting;
}
The Kotlin side declares the function external and loads the library
once:
class MainActivity : Activity() {
companion object {
init { System.loadLibrary("greeter") }
@JvmStatic external fun greet(name: String): String
}
}
With Gradle, put each ABI’s .so in src/main/jniLibs/<abi>/
(arm64-v8a, x86_64), or build it from the module’s CMakeLists.txt
with the static library as an imported target.
Threads
Exported functions may be called from any thread (see
Initialization, panics and threads),
so the app decides where Pepper code runs. Keep long work off the main
thread (iOS’s main queue, Android’s UI thread): call it from a background
queue, a coroutine on Dispatchers.Default, or a Pepper thread or task the
library starts itself. Pepper code never calls back into UIKit or Android’s
views on its own; hand results back to the main thread on the app’s side.
What works differently
The runtime runs the documentation’s examples on both platforms, with these differences:
- Starting processes. An iOS app cannot start other programs, and the
processmodule’sCommand.spawnreturns anIoErrorthere. On Android it works, but an app has few programs to start, and below API level 34Command.working_directorymakesspawnreturn anIoError(bionic lacksposix_spawn_file_actions_addchdir_npthere). - TLS. The
tlsmodule (and HTTPS) loads OpenSSL at run time, which neither platform ships, so making a TLS context returns anIoErrorunless the app bundleslibssl. Use the platform’s own networking (URLSession, OkHttp) for HTTPS for now. - Files. An app reads and writes inside its own container: pass the
path the platform gives (
FileManager’s documents directory,Context.getFilesDir()) to the Pepper code.os.home_directory()and the temporary directory point into the container on iOS. Android has no/tmp: withoutTMPDIR,io.Directory.temp()is/data/local/tmp, which an app cannot write, so an app passes its cache directory. A Unix socket’s path is at most 104 bytes there, and the container’s own path takes most of them, so give a socket there a short name. On Android, an app creates Unix socket files in its own data folder (Context.getFilesDir()); SELinux refuses them elsewhere, and from anadb shellprogram everywhere, with a permission error. - Backtraces. Android’s backtraces come from the NDK’s unwinder
(bionic’s
backtraceneeds API level 33), and both platforms name functions from the binary’s own symbols, so strip a release build only after keeping a copy for symbolication. - Standard output goes to the Xcode console and the simulator’s
--stdoutfile on iOS. On Android it goes nowhere in an app: log with the platform’s logger instead (__android_log_writethrough anextern).
Shipping
- Pepper compiles ahead of time, so iOS’s ban on JIT compilation does not apply. Bitcode is no longer required by the App Store.
- A static library is linked into the app’s own binary, so its code is
covered by the app’s signature; strip the release build’s symbols as
usual (Xcode’s Strip Linked Product, the Android Gradle plugin’s
stripofjniLibs). A Pepper library adds the runtime parts the program uses: the prototype app’s library below is about 1 MB unstripped. - Each Android ABI needs its own
.so; Google Play requiresarm64-v8a.
Debugging
-g writes DWARF, which LLDB reads in both Xcode and Android Studio. Build
the library with -g and the app in its debug configuration, and set
breakpoints in the .pep files by path.
The prototype apps
docs/examples/mobile/ holds a small app for each platform that calls the
same Pepper library, greeter.pep:
ios/: a SwiftUI app (PepperDemo.swift), andbuild.sh, which builds it withswiftcfor the simulator and for devices, installs it in a booted simulator, launches it and checks its output.android/: an activity in Java with the JNI functions injni.c, andbuild.sh, which builds the APK with the Android SDK’s own tools (javac,d8,aapt2,apksigner; Gradle is not needed), installs it in the emulator, starts it and checks its log.
scripts/mobile-test.sh runs both on a Mac with Xcode and the Android SDK:
it builds every documentation example for the iOS simulator and for Android,
runs each in the simulator (xcrun simctl spawn) and in the emulator
(adb shell), then builds and runs the two apps.
bash scripts/mobile-test.sh target/bin/pep-build /tmp/mobile all
Not supported yet
- TLS through the platform: Security.framework on iOS, and on Android the system’s BoringSSL through Conscrypt or a bundled OpenSSL.
- Generated bindings: the JNI layer and the Swift wrappers are written by hand; a generator could write them from the header.
- Packaging: an
.xcframeworkholding the device and simulator libraries, a Swift package, and an Android library (.aar). - Other targets: 32-bit Android (
armeabi-v7a,x86), Mac Catalyst, tvOS, watchOS and visionOS.
Build options
Some code should be optional at build time: linking a library such as
libcrypto or zlib when it is there, tracing that only a debug build wants,
a backend chosen when the program is built. Build options set that on
the command line with -D, as in C compilers, and the code tests them with
BuildOption.
pep-build main.pep -D USE_LIBCRYPTO -D LOG_LEVEL=2 -D BACKEND=zlib -o app
-D NAMEdefines an option. It counts asNAME=1, as in C.-D NAME=VALUEdefines it with a value, which may be any text.- Repeat
-Dfor more options. When two name the same option, the later one wins.
pep-build --test defines TEST, which keeps the [Test] functions and
can mark code only tests use (see Unit tests).
A name is a letter or _, then letters, digits and _. Any other name is
an error:
error: invalid build option name '2FAST' in '-D 2FAST'; a name is a letter or '_' followed by letters, digits and '_'
# Build options: compile with `pep-build main.pep -D FAST_MATH -D LOG_LEVEL=2`
# to pick the other versions.
# `[BuildOption("NAME")]` keeps a declaration when `-D NAME` is given, and
# `!BuildOption(NAME)` inside a `Target` keeps its fallback otherwise.
[BuildOption("FAST_MATH")]
scale(x: int): int {
return x << 1;
}
[Target(!BuildOption(FAST_MATH))]
scale(x: int): int {
return x * 2;
}
# A `static` block reads an option's value while compiling, with a default
# for a build that does not set it.
log_level(): int {
var level = 1;
static (level) {
if (reflect.build_option("LOG_LEVEL", "1") == "2") {
level = 2;
}
}
return level;
}
main(): int {
if (scale(21) != 42) {
return 1;
}
var level = log_level();
if (level < 1 || level > 2) {
return 2;
}
return 0;
}
Selecting code
[BuildOption("NAME")] keeps a declaration or a block only when the build
defines NAME. It is shorthand for [Target(BuildOption(NAME))], and it
goes everywhere [Target(...)] goes: functions, externs, structs,
methods, module-scope variables, imports, re-exports and { } blocks in
a function (see Platform selection).
Inside Target, BuildOption(NAME) combines with OS(...), Arch(...),
!, &&, || and parentheses. Used alone, it is true when NAME is
defined, whatever its value, and false when it is not, with no warning:
[BuildOption("USE_LIBCRYPTO")]
[Link("crypto")]
extern int SHA256_Init(__ptr<void> context);
[Target(!BuildOption(USE_LIBCRYPTO))]
_sha256_compress(...) { ... } # the portable fallback
[Target(BuildOption(TRACE) && OS(Linux))]
import "linux_trace";
Several attributes on one item combine with “and”, as [Target] does.
Options the runtime reads
| Option | Effect |
|---|---|
USE_LIBCRYPTO | digest’s SHA-256 and SHA-1 use libcrypto (CNG on Windows); see Cryptographic hashes |
USE_ZLIB | compress uses zlib; see Compression |
TEST | defined by pep-build --test; see Unit tests |
Comparing values
BuildOption(NAME) may be compared with a literal using ==, !=, <,
<=, > and >=, on either side. The literal’s type says how the value
is read:
| Literal | The value is read as | Example |
|---|---|---|
| integer | a signed 64-bit integer, decimal or 0x hex | BuildOption(LOG_LEVEL) >= 2 |
| float | a double | BuildOption(SCALE) > 1.5 |
true / false | true/false, 1/0, on/off or yes/no, in any case (== and != only) | BuildOption(TRACE) == true |
| string | text, compared byte for byte | BuildOption(BACKEND) == "zlib" |
[Target(BuildOption(LOG_LEVEL) >= 2 && OS(Linux))]
_trace(message: String) { ... }
[Target(BuildOption(BACKEND) == "zlib")]
import "zlib_backend";
A value the literal’s type cannot read is an error at the comparison:
main.pep:1:28: error: build option 'LEVEL' is 'high', which is not an integer
1 | [Target(BuildOption(LEVEL) >= 2)]
| ^~~~
Comparing two build options, or a build option with anything but a literal, is an error too.
A comparison with an option the build does not define is false, whatever the operator, and the compiler warns at it, since a missing option there is usually a mistake:
main.pep:3:13: warning: build option 'LOG_LEVEL' is not defined; the comparison is false
3 | [Target(BuildOption(LOG_LEVEL) >= 2)] {
| ^~~~~~~~~~~~~~~~~~~~~~
note: define it with '-D LOG_LEVEL=<value>'
Reading values in static blocks
A static block reads build options while compiling:
reflect.has_build_option("NAME")is whether the build definesNAME.reflect.build_option("NAME")is its value, as aString. It is an error when the build does not defineNAME.reflect.build_option("NAME", "default")is its value, ordefaultwhen the build does not define it.
The build cache and the editor
The options and their values are part of the build cache’s key, so building with other options never reuses objects built with different ones.
The language server analyzes with the options in the VS Code setting
pepper.buildOptions, a list written as -D takes them
(["USE_ZLIB", "LOG_LEVEL=2"]). It has none by default, so code that needs
an option shows dimmed, and comparisons with options warn until the
setting names them.
Not yet
- A
pep.tomltable declaring the options a package takes, with their types and defaults. - Arithmetic on options, and comparing two options.
Linking
The Pepper compiler turns your program into a native executable in two steps: it compiles your files and the runtime into object files (one per source file; see Output files), and then links them with the system libraries it needs. When you call C functions, linking is where their definitions come from, so this chapter explains what is linked by default and how to add more.
What the compiler links
The compiler uses clang as its link driver, rather than calling the
platform’s linker directly, because clang already knows each platform’s
startup files, C library and SDK locations. It uses the clang from the LLVM
installation the compiler was built with when there is one, and otherwise
the first clang on your PATH.
Every program is linked with:
- Linux: the C library, which clang adds, and the math library (
m), which the Pepper compiler adds (a floating-point**calls itspow), and, when the compiler runs on a glibc older than 2.34,pthreadanddl, where that glibc keeps threads anddlsym(a newer one has them in the C library). - macOS:
libSystem, which contains the C library, plus the SDK and platform-version flags. clang adds all of these, using the Xcode Command Line Tools. - Windows: the Microsoft C runtime (the UCRT,
vcruntimeand the CRT startup library), which clang adds, pluskernel32,user32andshell32, which the Pepper compiler adds. A program that usesasyncalso linkssynchronization, which the runtime names with[Link]for its locks (WaitOnAddress). clang finds the Visual Studio and Windows SDK libraries itself, so you do not need a Developer Command Prompt.
Any library file named lib<name>.a in the runtime directory is linked too.
Anything else must be linked explicitly. On Linux, that includes pthread
on older systems. Android keeps pthread and rt in its C library and has
no library of either name, so the compiler drops -l pthread and -l rt
when linking for Android: one [Link("pthread")] serves both.
Linking a library from the source: [Link]
Attach [Link("name")] (or [Link(lib = "name")]) to an extern
declaration to link the library that defines it. The compiler passes -lname to the linker, just like -l on the
command line. Name the library the way the linker does: m for libm,
gdi32 for gdi32.lib.
# [Link("name")] on an extern links the library that defines it.
# gdi32 is not one of the libraries Windows programs link by default.
[Target(OS(Windows))] [Link("gdi32")] extern ulong GetStockObject(int kind);
# pthread_self is in libpthread on older Linux systems, in libc on newer
# ones, and in libSystem on macOS; linking pthread works on all of them.
[Target(!OS(Windows))] [Link("pthread")] extern ulong pthread_self();
# The math library is separate from the C library on Linux.
[Target(OS(Linux))] [Link("m")] extern double pow(double base, double exponent);
[Target(!OS(Linux))] extern double pow(double base, double exponent);
main(): int {
[Target(OS(Windows))] {
# 5 is NULL_BRUSH, a stock object that always exists.
assert(GetStockObject(5) != 0, "gdi32 was linked");
}
[Target(!OS(Windows))] {
assert(pthread_self() != 0, "pthread was linked");
}
assert(pow(2.0, 10.0) == 1024.0, "the math library was linked");
return 0;
}
- The library is linked only if the extern survives
platform selection. Combining
[Link]with[Target], as above, links a library only on the platforms that need it. - Each library is linked once, however many externs name it.
- The library is linked even when nothing calls the extern.
- The same attribute can also bind the extern to a differently named C
symbol:
[Link(lib = "m", name = "cbrt")](see Binding to a different symbol name).
Putting [Link] in the source keeps the knowledge of which library a function
comes from next to its declaration, so nobody building your program has to
remember a command-line flag.
Linking a library from the command line: -l
The -l option links a library for one build:
pepper build main.pep -l m -o main
Repeat it for more libraries (-l m -l pthread).
A library the linker does not find on its own (one outside the system’s
library directories, such as a Homebrew install on macOS) needs its folder
on the search path. -L dir adds one, like -L for clang:
pepper build main.pep -L /opt/homebrew/opt/llvm@20/lib -o main
Repeat it for more folders. The folders are searched for -l libraries and
[Link] libraries alike, in the order given, before the default ones. On
Windows, clang passes each one to the linker as /LIBPATH:.
Library search directories in the source and the environment
A library that ships with your program, such as a vendored build in a
vendor/ folder, can name its folder in the [Link] attribute with path:
[Link(lib = "probe", path = "vendor/lib")]
extern int probe_version();
A relative path is relative to the folder of the file that declares the
extern, not to where you run the compiler, so the program builds the same
from anywhere. Like the library, the folder is searched only when the extern
survives platform selection, so a [Target] can
give each platform its own folder.
The PEPPER_LIBRARY_PATH environment variable adds folders for every build,
separated like PATH (:, or ; on Windows):
PEPPER_LIBRARY_PATH=/opt/homebrew/lib pepper build main.pep -o main
The linker searches -L folders first, then [Link] paths, then
PEPPER_LIBRARY_PATH, then the runtime’s folder and the system’s.
When linking fails
If a function you declared with extern is not defined by any linked library,
compilation succeeds and the link fails. The compiler prints what the linker
said, which names the missing symbol:
error: Link failed: clang exited with exit status: 1
/usr/bin/ld: .pep/build/main.units/main.o: in function `main':
main:(.text+0x25ba): undefined reference to `no_such_function'
clang: error: linker command failed with exit code 1 (use -v to see invocation)
The object the linker names is the one for the file that made the call, in
.pep/build/<name>.units/. Symbols of your own functions appear as the
compiler spells them for the linker: main and a method such as
Point.length keep their names, and any other name is written _P followed
by the name with _ doubled and other characters as _ and two hex digits,
so a function parse appears as _Pparse and Array<int>.count as
_PArray_3cint_3e.count. That way a Pepper function never collides with a C
library function of the same name, such as exit. extern functions keep
their C names.
To fix it, check the spelling of the extern against the C function (or use
[Link(name = "symbol")]
when the names differ), then add the library that defines it with [Link] or -l.
If the compiler cannot run clang at all, the error says
Link failed: cannot run clang. Install clang (on Linux, from your package
manager) or the Xcode Command Line Tools on macOS.
Warnings from the linker are passed through unchanged, even when the link succeeds.
Output files
With no -o, the executable is named after the first source file, without an
extension, in the directory where you run the compiler. Windows will not run a
file without .exe, so pass -o main.exe there. The object files and other
intermediate files go in .pep/build/ under the current directory, or in the
directory you pass to --build: one object per file of the program in
<name>.units/, and <name>.o, which runs the program’s global
initializers. Unchanged objects come from the build cache in .pep/cache/
(--no-cache skips it).
With --lib static or --lib shared, the output is a library and a C
header instead (see Building the library).
Compiler errors
When the compiler cannot build your program, it tells you what is wrong and exactly where, in the same format as clang. This chapter shows how to read an error and lists some common ones.
Reading an error
Here is a program with a mistake in it:
struct Point {
var x: int;
var y: int;
}
main(): int {
var p = Point();
var total = p.x + p.z;
return total;
}
Compiling it prints:
main.pep:8:25: error: no member named 'z' in 'Point'
8 | var total = p.x + p.z;
| ^
An error has three parts:
- The header: the file, line and column, then
error:and the message. Editors and terminals recognizefile:line:columnand can jump to it. - The source line the error is on, with its line number.
- A caret (
^) under the exact token you need to change, with~under the rest of the token when it is longer than one character. Here, the member namez, not the whole statement.
Messages follow clang’s style: they start in lowercase, put names and types in
single quotes ('z', 'Point'), and say what to do when there is an obvious
fix. The fixed program is:
# The program from "Reading an error", after the fix.
struct Point {
var x: int;
var y: int;
}
main(): int {
var p = Point();
p.x = 3;
p.y = 4;
var total = p.x + p.y;
assert(total == 7, "x + y");
return 0;
}
Notes
Some errors are explained by a second place in your code. The compiler shows
that place as a note: after the error, in the same format. When one file
calls a function that another file did not export, the note points at the
declaration:
main.pep:4:12: error: 'helper' is private to lib.pep; declare it with 'export' to use it from another file
4 | return helper();
| ^~~~~~
lib.pep:1:1: note: declared here
1 | helper(): int { return 1; }
| ^~~~~~
When a brace is never closed, the error is at the end of the file, and the note shows the brace that opened the block:
main.pep:5:1: error: expected '}'
5 |
| ^
main.pep:1:13: note: to match this '{'
1 | main(): int {
| ^
One error at a time
The compiler stops at the first error it finds and exits with status 1, so fix that error and compile again. It never produces an executable from a program with an error.
On a terminal, errors are colored like clang’s. Set the NO_COLOR environment
variable to turn colors off. Output that goes to a file or a pipe is never
colored.
Warnings do not stop the build, and the compiler prints them whether or not
it also finds an error. Besides the linker’s own warnings, which the compiler
passes through (see Linking), the warnings are: deprecations;
a method whose parameter names differ from its trait’s (see
Traits); an else arm of a match that
every case already handles (see Enums); a comparison with
a build option that is not defined (see
Build options); and an [Export] that
has no effect, or a --lib build that exports nothing (see
Calling Pepper from C).
Deprecations
A use of a declaration marked [Deprecated("message")] warns at the name,
with the message and a note at the declaration. When the attribute names a
replacement, the warning shows the rename under the code, and the language
server offers it as a quick fix. See Deprecation.
main.pep:9:15: warning: 'clone' is deprecated: use 'copy' instead [deprecated]
9 | var b = a.clone();
| ^~~~~
| copy
main.pep:4:5: note: 'clone' declared here
4 | clone(): Counter { return this.copy(); }
| ^~~~~
Syntax of the language itself that is on its way out keeps compiling for a while too, with a warning that names its replacement and shows the fix under the code it replaces:
main.pep:3:5: warning: `new` is deprecated; constructors are spelled `construct`, matching `destruct` [deprecated]
3 | new(this.x = 1) {}
| ^~~
| construct
| Deprecated | Replacement |
|---|---|
new(...) constructor | construct(...) |
| A struct, enum, type alias, trait or variable that two files of one module both declare, privately (now an error: files of a module share their names) | one declaration, or two names |
| A declaration of another file of the module, used without importing that file (now an error) | import "file"; in the file that uses it |
io.Path.join_str(String) | io.Path.join(Path), which accepts a String |
String.clone() | String.copy() |
ThreadLocal<T>.get() for a T that is not thread-safe (now an error) | with (var value = local.enter()) { ... } |
io.Path.is_dir() | io.Path.is_directory() |
io.File.set_len(ulong) | io.File.set_length(ulong) |
io.File.write_string(String) | io.File.write_all(String) |
io.TcpStream.write_string(String), io.UnixStream.write_string(String), tls.TlsStream.write_string(String) | write_all(String) |
io.File.seek(Offset, int) | io.File.seek(io.SeekFrom) |
io.File.std_file(int, String) | io.File.std_file(io.StdStream, String) |
HashMap.cap(), HashSet.cap() | the capacity property |
arguments(), exit(code) in the prelude | process.arguments(), process.exit(code) (import process) |
Channel.send(x), try_send(x) with a variable something else references (now an error: send no longer copies) | send(move(x)), or send(copy(x)) to keep using x |
[Sending] parameter attribute (now an error) | [Move] |
io.Print trait | io.Writer (which also needs write(data)) |
Atomic<T> without an import (now an error: it moved to the async module) | async.Atomic<T> after import async, or import [Atomic] from async |
math.parse_int(text, base) | long.parse(text, base), with no import |
Optional<T>() (now an error: Optional is an enum) | Optional<T>.Empty |
Optional cases None and Some(v) (now an error: renamed) | Empty and Value(v) |
Optional<T>.some(v), Optional<T>.none() | the cases Optional<T>.Value(v), Optional<T>.Empty |
Optional.is_some(), Optional.is_none() | is_value(), is_empty() |
[LinkName("symbol")] on an extern (now an error) | [Link(name = "symbol")] |
A parameter’s former name, kept by [Rename("old", "new")] | the new name (see named arguments) |
Copyable<T> implemented as copy(copies: Copies): T, with copies.copy(x) and copies.register(original, made) | copy(): T, with copy(x) and register_copy(original, made) (see copying) |
hint.likely(condition), hint.unlikely(condition) | [Likely] or [Unlikely] above the if or match arm (see branch hints) |
Errors in the editor
The Pepper language server, used by the VS Code extension, reports the same errors at the same places while you type. It underlines the token the caret would point at, and shows each note as related information that you can click to jump to.
Common errors
These are some errors you are likely to meet, with the exact text the compiler prints.
A name that was never declared, often a typo:
main.pep:3:16: error: use of undeclared identifier 'nope'
3 | return a + nope;
| ^~~~
main.pep:2:12: error: call to undeclared function 'frob'
2 | return frob(1);
| ^~~~
A type name that does not exist, often a misspelled type or a missing import:
main.pep:2:12: error: unknown type name 'Frob'
2 | var x: Frob = 1;
| ^~~~
A field or method the struct does not have:
main.pep:6:7: error: no member named 'move' in 'Point'
6 | p.move(1);
| ^~~~
The wrong number of arguments. When the function has default parameters, the message gives the range:
main.pep:4:12: error: too few arguments to 'add': expected 2, have 1
4 | return add(1);
| ^~~
main.pep:3:12: error: too many arguments to 'f': expected at most 2, have 3
3 | return f(1, 2, 3) + f();
| ^
A call no overload fits, or that two fit equally well. Each note is a candidate, and says why it does not fit (see Overloading):
main.pep:3:22: error: call to 'f' is ambiguous
3 | main(): int { return f(1); }
| ^
main.pep:1:1: note: candidate function
1 | f(x: long): int { return 1; }
| ^
main.pep:2:1: note: candidate function
2 | f(x: short): int { return 2; }
| ^
A struct used as a condition without a truth-test operator. A struct is
never tested for null implicitly: declare !!(): bool on it, or compare with
__null (see Truth tests and conditions):
main.pep:6:9: error: 'Node' cannot be used as a condition: declare '!!(): bool' on it, or compare with '__null'
6 | if (node) {
| ^~~~
main.pep:1:8: note: 'Node' declared here
1 | struct Node {
| ^~~~
A syntax error. The parser says what it expected at the first place the code stopped making sense:
main.pep:2:14: error: expected ')'
2 | if (true {
| ^
main.pep:2:13: error: expected expression
2 | var x = ;
| ^
A value of the wrong type. Numbers convert among themselves, as in C, but
a number is never a struct and one struct type is never another, except that
a struct converts to a trait it declares,
and a value converts to a struct whose constructor from its type is
[Implicit].
A raw pointer is never a struct either: a __ptr<char> is not a String
(copy it with String(p)), and a String is not a __ptr<char> (pass its
c_str()).
Two specializations of one generic struct are different types: Array<int>
is not Array<String> and Array<Derived> is not Array<Base>. Type
arguments compare exactly, at every depth, after aliases resolve and omitted
arguments take their defaults (Result<int> is Result<int, Error>).
The check applies wherever a value flows into a typed place: a variable’s
initializer, an assignment, an argument, a return, and each arm of
c ? a : b, whose false arm must convert to the true arm’s type:
main.pep:2:13: error: cannot initialize variable 'x' of type 'int' with a value of type 'String'
2 | int x = "hello";
| ^~~~~~~
main.pep:3:12: error: cannot pass a value of type 'i32' as argument 1 of 'g', which has type 'String'
3 | return g(5);
| ^
main.pep:3:19: error: cannot pass a value of type '__ptr<char>' as argument 2 of 'assert', which has type 'String'; use 'String(...)' to copy a C string
3 | assert(false, p);
| ^
main.pep:2:12: error: cannot return a value of type 'String' from 'f', which returns 'int'
2 | return "a";
| ^~~
main.pep:2:20: error: cannot initialize variable 'a' of type 'Array<int>' with a value of type 'Array<String>'
2 | Array<int> a = Array<String>();
| ^~~~~~~~~~~~~
main.pep:3:35: error: incompatible operand types ('Array<int>' and 'Array<String>')
3 | var a = flag ? Array<int>() : Array<String>();
| ^~~~~~~~~~~~~
A struct used as a trait it does not declare (see Traits as types):
main.pep:11:20: error: 'Circle' does not conform to trait 'Shape'
11 | var s: Shape = Circle();
| ^~~~~~
main.pep:6:8: note: declare it with 'extend Circle with Shape'
6 | struct Circle {
| ^~~~~~
A value that is not thread-safe given to another thread (see
Threads): here a thread body, a
shared function value, that captures a String:
main.pep:5:46: error: closure passed as 'shared () => void' captures 'name' of type 'String', which is not thread-safe
5 | var t = async.Thread.spawn(() => { print(name); });
| ^~~~
main.pep:4:9: note: capture a thread-safe value instead, such as an 'async.Atomic<T>' or a '[Shared]' struct
4 | var name = "worker";
| ^~~~
The same name declared twice in one scope:
main.pep:3:9: error: redefinition of 'n'
3 | var n = 2;
| ^
main.pep:2:9: note: previous definition is here
2 | var n = 1;
| ^
The same holds for top-level types: two structs, traits, enums or type
aliases of one name in a file, or exported by two
prelude modules, report
redefinition of 'Point'.
Two functions that differ only in their return type, in one file or beside a runtime function of the name, which a call could not tell apart:
main.pep:1:1: error: functions that differ only in their return type cannot be overloaded
1 | to_string(value: int): int { return value; }
| ^~~~~~~~~
runtime/string.pep:1621:8: note: previous declaration is here
1621 | export to_string(value: int): String {
| ^~~~~~~~~
Looping over something that cannot be iterated (see Iteration):
main.pep:3:19: error: type 'int' is not iterable: implement Iterable<T> (iterate()) or Iterator<T> (next() and current())
3 | for (var i in 5) {
| ^
An import that names nothing (see Modules and imports):
main.pep:1:8: error: cannot find "nope.pep" or the module folder "nope" next to this file
1 | import "nope";
| ^~~~~~
Code nested too deeply. Parentheses, blocks, calls, and each operator of
a chain (a + b + c is two levels) nest the program one level deeper, and a
file may nest at most 256 levels. The error points at the token that crosses
the limit, here the 255th + of a long sum:
main.pep:2:1030: error: code is nested too deeply: the limit is 256 levels; split it into smaller expressions or functions
2 | return 0 + 1 + 1 + ... + 1;
| ^
Split the expression into steps (var part = ...;), or move the inner part
into a function.
A generic that asks for a larger copy of itself (see
Generics). Each type argument makes a separate copy of a
generic, so a Foo<T> holding a Foo<Array<T>> never runs out of copies to
make. The compiler stops 64 levels deep, at the type that asks for the next
level, with a note where the chain started:
main.pep:2:16: error: 'Foo' is specialized 65 levels deep, past the limit of 64; a generic whose instance asks for a larger instance of itself never ends
2 | var inner: Foo<Array<T>>;
| ^~~~~~~~~~~~~
main.pep:6:13: note: asked for by 'main' here
6 | var f = Foo<int>();
| ^~~~~~~~
A cast that reinterprets memory. as converts values, like static_cast
(see Casting with as); a pointer to
an integer, or to a pointer of another type, needs
unsafe_cast:
main.pep:3:15: error: cannot cast '__ptr<int>' to 'ulong' with 'as'; use 'unsafe_cast<ulong>(...)' to reinterpret
3 | var a = p as ulong;
| ^~
Errors while the program runs
Some problems can only be found while the program runs, such as an array index
out of range, an integer overflow, a
division by zero, or a failed assert. These are not compiler errors: the program
prints a message and stops immediately (it aborts, so it exits with a failure
status instead of returning from main). See assert and
panic.
Deprecation
When an API is on its way out, mark it [Deprecated("message")]. Code that
uses it still compiles, but every use gets a warning that shows your message,
the same way clang reports [[deprecated]]. That gives the code that depends
on the API time to move before you remove it.
struct Temperature {
int celsius;
# A method on its way out: it still works, but every call warns.
[Deprecated("use 'fahrenheit' instead", "fahrenheit")]
to_f(): int { return fahrenheit(); }
fahrenheit(): int { return celsius * 9 / 5 + 32; }
}
# The second argument names the replacement: the warning carries a fix that
# renames the call.
[Deprecated("use 'boiling' instead", "boiling")]
boiling_point(): Temperature { return boiling(); }
boiling(): Temperature {
var t = Temperature();
t.celsius = 100;
return t;
}
main(): int {
# Both calls warn, and both still work.
var old = boiling_point();
assert(old.to_f() == 212, "the old API still works");
var t = boiling();
assert(t.fahrenheit() == 212, "100 C is 212 F");
return 0;
}
Compiling this prints a warning at each use of a deprecated name, with a note at its declaration. The second of the two is:
main.pep:25:16: warning: 'to_f' is deprecated: use 'fahrenheit' instead [deprecated]
25 | assert(old.to_f() == 212, "the old API still works");
| ^~~~
| fahrenheit
main.pep:6:5: note: 'to_f' declared here
6 | to_f(): int { return fahrenheit(); }
| ^~~~
Warnings never stop the build. The program above compiles and runs.
The replacement
The attribute takes an optional second argument: the name that replaces the deprecated one.
[Deprecated("use 'copy' instead", "copy")]
clone(): Counter { ... }
With a replacement, the warning comes with a fix that renames the use. The compiler shows the new name under the old one, as in the warning above, and the language server offers the rename as a quick fix. The fix only renames, so give a replacement only when the new declaration can be used the same way. Otherwise leave it out and explain the change in the message.
An empty message is allowed. The warning then says only 'name' is deprecated.
What it applies to
Put [Deprecated(...)] before any declaration other code refers to by name:
- a function or an
extern - a struct, a trait, or a type alias
- a module-scope variable
- a field, a method, or a constructor of a struct, or a method of an enum
- a method in a trait or in an extension (put it on each method, not on the
extendblock)
A doc comment goes above the attribute, as it does for any attribute.
To rename a parameter while calls still name the old
one, put [Rename("old", "new")] on the
function instead (see Other names for a parameter).
Which uses warn
Every use of the declaration warns, at the name you wrote:
- a call, a reference to a function or variable, or a construction
S()(a deprecated constructor is reported at the struct’s name) - a type name, anywhere a type is written
- a member access
x.name, a method callx.name(), a static callS.name(), and a bare field or method name inside the struct’s own methods - an entry in an import list,
import [name] from m, and a qualified usem.name, where the warning points atname - a call the compiler writes for you, at the token that stands for it: a
forloop’snext()andcurrent()atfor, awithblock’senter()atwith, and an operator overload at its operator (a + bat+; a deprecated==also warns at a!=that uses it). These warnings offer no rename fix.
These do not warn:
- the declaration itself
- a use inside a declaration that is deprecated too, including the methods of a deprecated struct and closures written inside a deprecated function, so old code can keep calling old code without noise
- a re-export,
export name as otherorexport [name] from source, which passes the name on without using it
Mistakes in the attribute
A malformed attribute is an error at the attribute:
main.pep:1:1: error: 'Deprecated' expects a message string: [Deprecated("message")]
1 | [Deprecated]
| ^~~~~~~~~~~~
The other errors are 'Deprecated' expects the replacement as a string, the replacement 'a b' is not a name, too many arguments to 'Deprecated': expected at most 2, have 3, duplicate 'Deprecated' attribute, and
'Deprecated' cannot be applied to an extension or a destructor.
Documentation comments
Comments explain code to the people who read it. Pepper has one comment
syntax, #, and a convention on top of it for documentation: lines starting
with ## directly above a declaration document that declaration. Your editor
shows them when you hover over a use of the name, and the
standard library reference in this book is generated
from them.
Comments
A comment starts with # and runs to the end of the line. It can take a whole
line or follow code:
# Count the lines that are not blank.
var count = 0; # blank lines are skipped below
Pepper has no block comment syntax: to comment out several lines, start each
with #. (// is not a comment in Pepper.)
Doc comments
A doc comment is a run of consecutive lines that start with ##, directly
above the declaration it documents:
## Returns the larger of `a` and `b`.
##
## @param a the first value
## @param b the second value
## @returns `a` when it is at least `b`, otherwise `b`
larger(a: int, b: int): int {
return a >= b ? a : b;
}
- The text of each line is what follows
##. A line that is just##is a blank line in the documentation, which separates paragraphs. - The comment must sit directly above the declaration, with no blank line in
between. Attributes and
exportorstaticmay come between the comment and the declaration, so put the comment above the attributes. - Only lines that start with exactly two
#count. A single#is an ordinary comment, and a line starting with three or more, such as a decorative#######, is not documentation. - A doc comment works on functions, structs, enums and their cases, traits, extensions, type aliases, module-scope variables, and the fields, properties, constructors and methods inside a struct, enum, trait or extension.
What to write
Start with a description: what the declaration does, in the third person (“Returns …”, “Appends …”), followed by what a caller needs to know, such as edge cases, who owns what, how long it takes, and differences between platforms. The description is Markdown, so put code in backticks.
After the description come tags, each starting a line with @. A tag’s
text runs until the next tag.
| Tag | Use it for |
|---|---|
@typeParam T description | each generic parameter of a generic struct, trait, type alias or function |
@param name description | each parameter of a function, method or constructor |
@returns description | what the function returns, whenever it returns a value |
@panics description | each way a caller can make the function panic (a failed assert, a call to panic) |
@example | sample code, indented on the lines that follow; line breaks are kept |
@see name | a related declaration |
@note text | a remark that does not fit elsewhere |
Write tag descriptions like clang writes errors: lowercase start, no trailing
period. A field or a type alias usually needs only one line:
## The number of bytes in the buffer.
Keep notes that matter only to the people maintaining the code in ordinary #
comments inside the body. The doc comment is the contract with callers.
Here is a small documented library:
# A small library, documented the way the standard library is.
# This line and the one above are ordinary comments: they start with a
# single '#'. Doc comments start with '##'.
## A last-in, first-out collection.
##
## Elements come out in the reverse of the order they went in.
##
## @typeParam T the type of the elements
## @example
## var s = Stack<int>();
## s.push(1);
## s.push(2);
## var top = s.pop(); # top == 2
struct Stack<T> {
## The elements, bottom first.
var items: Array<T> = Array<T>();
## Adds `value` on top of the stack.
##
## @param value the element to add
push(value: T) {
items.append(value);
}
## Removes and returns the element on top of the stack.
##
## @returns the element that was pushed last
## @panics if the stack is empty
## @see Stack.push
pop(): T {
assert(items.count > 0, "pop() on an empty Stack");
return items.remove_at(items.count - 1);
}
## Returns the number of elements.
##
## @returns how many elements the stack holds
count(): ulong {
return items.count;
}
}
## Returns the larger of `a` and `b`.
##
## @param a the first value
## @param b the second value
## @returns `a` when it is at least `b`, otherwise `b`
## @note ties return `a`
larger(a: int, b: int): int {
# An implementation note for maintainers, not part of the docs.
return a >= b ? a : b;
}
main(): int {
var s = Stack<int>();
s.push(1);
s.push(2);
assert(s.pop() == 2, "the last element comes out first");
assert(s.count() == 1, "one element is left");
assert(larger(3, 7) == 7, "larger returns the larger value");
return 0;
}
Where doc comments appear
- In your editor. The Pepper language server shows a declaration’s doc comment when you hover over its name, anywhere it is used. The description comes first, followed by sections for the type parameters, parameters, return value, panics, related declarations, notes and examples.
- In the standard library reference. The
reference pages are generated from the doc comments
in the runtime, one page per runtime file. A
##block at the very top of a file, followed by a blank line, introduces that file’s page. Building this documentation explains how to regenerate them.
The standard library is fully documented
Every public declaration in the runtime must have a doc comment: every
exported declaration whose name does not start with _, and every field,
constructor and method of such a struct, trait or extension whose name does
not start with _. Names starting with _ are internals shared between
runtime files and are exempt.
A test enforces this, and reports a clang-style error for each gap: a missing
doc comment, a comment with no description, a missing @param, @typeParam
or @returns, a tag with no description, a @returns on a function that
returns nothing, or a @param naming a parameter that does not exist. If you
contribute to the runtime, run the test suite before you send a change. The
compiler does not check the doc comments in your own programs, but following
the same format gets you the same editor support.
Naming conventions
Pepper code, including the standard library, follows one set of naming rules. The compiler does not enforce them, but every example in this book, the runtime, and the tools in this repository follow them, and code you contribute should too.
| What | Style | Examples |
|---|---|---|
| Structs, traits, type aliases | PascalCase | String, HashMap, IoResult |
| Generic parameters | one capital letter, or PascalCase | T, K, V, TValue, TError |
| Data members (fields) | snake_case | unit_name, is_file |
| Methods and free functions | snake_case | append, remove_at, read_to_string |
| Parameters and local variables | snake_case | capacity, best_so_far |
Constants (a const holding a fixed value) | SCREAMING_CASE | MAX_REQUESTS, LINE_WIDTH |
| Other module-scope variables | snake_case | request_count |
| Private data members and methods | _ then snake_case | _readings, _is_empty |
# Types are PascalCase; acronyms are written as words (`HttpClient`, not `HTTPClient`).
struct TemperatureLog {
# Public data members are snake_case.
var unit_name: String = "celsius";
# Private data members start with `_`.
var _readings: Array<int> = Array<int>();
var _total: int = 0;
# Methods and their parameters are snake_case.
add_reading(degrees: int) {
_readings.append(degrees);
_total = _total + degrees;
}
average(): int {
return _is_empty() ? 0 : _total / (_readings.count as int);
}
# A private method starts with `_` too.
_is_empty(): bool {
return _readings.count == 0;
}
}
# Generic parameters are a single capital letter or PascalCase.
struct Pair<TFirst, TSecond> {
var first: TFirst;
var second: TSecond;
construct(TFirst first, TSecond second) {
this.first = first;
this.second = second;
}
}
# Free functions and local variables are snake_case.
largest_of<T>(values: Array<T>): T {
var best_so_far = values[0];
for (var value in values) {
if (value > best_so_far) {
best_so_far = value;
}
}
return best_so_far;
}
main(): int {
var log = TemperatureLog();
log.add_reading(20);
log.add_reading(24);
assert(log.average() == 22, "average of the readings");
assert(log.unit_name == "celsius", "public member");
var pair = Pair<String, int>("answer", 42);
assert(pair.second == 42, "generic struct");
var numbers = Array<int>();
numbers.append(3);
numbers.append(9);
numbers.append(4);
assert(largest_of<int>(numbers) == 9, "generic function");
return 0;
}
Types
A type name is PascalCase: each word starts with a capital letter, and there
are no underscores. Write an acronym as a word, so IoError and HttpClient,
not IOError or HTTPClient.
An enum’s cases are PascalCase too (Color.Red, Status.NotFound).
A generic parameter is either a single capital letter (T, U, K, V) or
a PascalCase name, which by convention starts with T (TValue,
TError). Use a single letter when the parameter’s role is obvious, and a
name when a declaration has several parameters that a reader has to tell
apart.
Members, functions, and variables
Everything else a program names is snake_case: lowercase words joined by
underscores. That covers a struct’s data members, its methods, free
functions, parameters, local variables, and module-scope variables.
The exception is a constant: a const, at module scope or in a function,
whose value is fixed, such as a number, string, or character. Its name is
SCREAMING_CASE, uppercase words joined by underscores (MAX_REQUESTS,
LINE_WIDTH). A module-scope const that holds an object, such as
const request_count = async.Atomic<int>(0);, is a variable and stays
snake_case.
Spell words out. A name is read far more often than it is typed, so prefer
iterate, index, and directory to iter, idx, and dir. Keep an
abbreviation only when it is the usual name for the thing, such as io or
c_str.
An extern declaration keeps the name the C library gives the function,
whatever its style (GetCurrentProcessId), because that name is what the
linker looks up. The same goes for the fields of a struct that mirrors a C
struct.
Built-in type names
Unlike the conventions above, this rule is enforced. No top-level
declaration may take the name of a built-in type: void, bool,
char, byte, short, ushort, int, uint, long, ulong, float,
double, and their sized spellings (i8, u8, i16, u16, i32, u32,
i64, u64, f32, f64). Names resolve by spelling across the whole
program, so such a declaration would take the type’s place in every file,
the runtime’s included. The compiler reports it at the name:
main.pep:1:7: error: 'double' is the name of a built-in type; choose another name
1 | const double = (x: int): int => x * 2;
| ^~~~~~
Locals, parameters and members may still use these names, since they are visible only where they are declared.
Private members
Pepper’s private keyword applies only to top-level declarations (see
Visibility); any code that can reach an object can name all
of its members. A leading _ marks a data member or
method as private to its struct instead: code outside the struct, and its
extensions, should not use it. The rest of the name is snake_case, as for a
public member (_readings, _is_empty).
The standard library relies on this convention: its generated reference
leaves _ members out, and doc comments are required only
on members without the prefix.
A top-level declaration does not need the prefix to be private: it is
internal to its module (or, outside any module, private to its file) unless
it is exported, and private keeps it to its file (see
Visibility). The runtime also uses the
prefix on top-level helpers it exports but that are not part of its public
API, such as _pep_malloc, which the compiler calls by name, or a helper
one runtime module shares with another: the reference leaves them out too.
Installing Pepper
Each release publishes the Pepper toolchain prebuilt for Linux, macOS and Windows, with an install script that downloads the right one. You need no LLVM development install to use it; building from source is for working on Pepper itself (see Getting started).
# Linux and macOS
curl -fsSL https://raw.githubusercontent.com/justinboswell/pepper/main/scripts/install.sh | sh
# Windows (PowerShell)
irm https://raw.githubusercontent.com/justinboswell/pepper/main/scripts/install.ps1 | iex
Each release also carries both scripts as assets, beside its archives.
The script only bootstraps. It picks this machine’s archives
(linux-x64, macos-arm64, macos-x64 or windows-x64) from the newest
release that has them: the toolchain, pepper-<version>-<target>.tar.gz
(.zip on Windows), and the installer, pepper-install-<version>-<target>.tar.gz,
which holds only pep-install. It downloads both, checks the installer
against its .sha256, unpacks pep-install into a temporary folder and
runs pep-install setup --archive <toolchain>,
which:
- checks the toolchain archive against its
.sha256; - unpacks it into
~/.pepper/toolchains/<version>(%LOCALAPPDATA%\pepperon Windows), makes it the default toolchain, and puts the entry pointpepperandpep-installin~/.pepper/bin; toolchains installed before stay (see Toolchains: pep-install); - adds that folder’s
binto thePATH: in~/.profile,~/.bashrc,~/.bash_profileand~/.zshrcand fish’sconf.d, or in the Windows user’sPATH; - checks that programs can be linked (below), offering to install what is missing;
- builds and runs a small program with the new install, and fails if it cannot.
The script downloads the toolchain itself, rather than leaving it to
pep-install, because pep-install downloads with OpenSSL 3, which a fresh
Mac and RHEL 8 do not have.
Open a new terminal afterwards so the PATH change takes effect, then
pepper build hello.pep -o hello works as in Getting started.
What it installs
~/.pepper/
bin/
pepper the entry point (The pepper command), which runs a toolchain's tools
pep-install installs and removes toolchains (pepper install)
toolchains/
0.4.0/
bin/
pep-build the compiler, with LLVM inside it (on Windows, LLVM-C.dll beside it)
pep-fmt pep-test pep-package pep-profile pep-run pep-lsp pep-doc pepper pep-install
runtime/ the runtime, which pep-build finds from its own folder
LICENSE.md
default-toolchain the default toolchain's version
pepper runs the tools of the toolchain a folder uses: the default, unless
pepper +<version>, PEPPER_TOOLCHAIN or the project’s pep.toml names
another (see Toolchains). pepper install add,
use and update install more and change the default; update also
updates pepper and pep-install themselves.
Running the script again installs the newest release beside the others
and makes it the default. Only releases that carry the installer archive
can be installed this way; pepper install add installs older ones into a
home that is set up. A release from before toolchains (0.3.0 and earlier)
cannot be set up at all, and neither is an install from before toolchains,
with bin and runtime straight in ~/.pepper, moved: uninstall it
first.
On Linux the binaries need glibc 2.28 or newer (RHEL 8, Debian 10, Ubuntu 18.10 and later); systems that use musl, such as Alpine, are not supported. There are no prebuilt binaries for Linux or Windows on arm64 yet. The macOS binaries are not signed: an archive downloaded with a browser makes Gatekeeper warn before it runs; one the script downloads does not.
What linking needs
pep-build compiles with the LLVM inside it, but links each program with
clang, which also needs the platform’s linker and C library:
| Platform | Needs | The script offers |
|---|---|---|
| Linux | clang and the C library’s development files | apt-get install clang build-essential, dnf/yum install clang gcc glibc-devel, pacman -S clang gcc or zypper install clang gcc glibc-devel, whichever manager is present |
| macOS | Xcode’s command line tools | xcode-select --install |
| Windows | LLVM (clang) and the MSVC libraries and Windows SDK | LLVM with winget install LLVM.LLVM, else scoop install llvm; the Visual Studio Build Tools’ C++ workload with winget |
It asks before installing anything, unless given --yes (-Yes). With no
terminal to ask on it installs nothing and says what to install.
Programs that use the tls module also need OpenSSL 3 (libssl.so.3 on
Linux, Homebrew’s openssl@3 on macOS; Windows uses SChannel). The script
says so when it finds none, and on macOS offers brew install openssl@3.
pep-build looks for clang in this order, and uses the first it finds:
- in the LLVM named by
PEPPER_LLVM_PREFIX, else byLLVM_SYS_<major>1_PREFIX(LLVM_SYS_201_PREFIXfor LLVM 20); - beside
pep-builditself; - on the
PATH, asclang, then asclang-<major>for the LLVM it is built with (what Ubuntu’sclang-20package installs); - where LLVM’s packages put it when they leave it off the
PATH:C:\Program Files\LLVM\binand Scoop’sllvmon Windows; Homebrew’sllvmandllvm@<major>on macOS;/usr/lib/llvm-<major>/bin, then the newest/usr/lib/llvm-*/bin, on Linux.
When there is none, linking fails with an error that names what to install.
Options
install.sh [--version <x.y.z> | --archive <file>] [--prefix <dir>]
[--yes] [--no-modify-path] [--no-prerequisites]
install.sh --uninstall [--prefix <dir>]
install.sh | install.ps1 | What it does |
|---|---|---|
--version <x.y.z> | -Version <x.y.z> | Installs that release instead of the newest, or a prerelease such as 0.4.0-dev, the build of main (see below), which installing again refreshes |
--archive <file> | -Archive <file> | Installs an archive already downloaded, checked against <file>.sha256 beside it, with the installer archive pepper-install-<version>-<target> (and its .sha256) beside it too. One not named for a release, such as a dev build, installs as the version its pep-build --version reports |
--prefix <dir> | -Prefix <dir> | Installs into <dir> instead of ~/.pepper (%LOCALAPPDATA%\pepper) |
-y, --yes | -Yes | Installs missing prerequisites without asking |
--no-modify-path | -NoModifyPath | Leaves the PATH alone |
--no-prerequisites | -NoPrerequisites | Neither checks for nor installs what linking needs |
--uninstall | -Uninstall | Removes the install, every toolchain in it, and the PATH entries the script added, with the home’s own pep-install uninstall, else with the newest release’s installer |
-h, --help | Get-Help .\install.ps1 | Shows the usage |
Piped into a shell, the script still takes options: curl ... | sh -s -- --version 0.4.0.
irm ... | iex takes none, so install.ps1 reads two environment variables
in their place:
| Variable | Instead of |
|---|---|
PEPPER_HOME | --prefix / -Prefix (both scripts) |
PEPPER_VERSION | -Version (install.ps1) |
While the repository is private, its releases need a GitHub token to
download: set GITHUB_TOKEN (or GH_TOKEN) to one that can read it, for
example GITHUB_TOKEN=$(gh auth token). The raw script URLs above need it too,
so download the script with gh first:
gh api repos/justinboswell/pepper/contents/scripts/install.sh -H "Accept: application/vnd.github.raw" > install.sh
GITHUB_TOKEN=$(gh auth token) sh install.sh
Uninstalling
sh install.sh --uninstall # or: curl ... | sh -s -- --uninstall
.\install.ps1 -Uninstall
This removes the install folder (only when it holds bin/pep-build) and the
PATH lines the script added. Prerequisites it installed, such as clang,
stay.
How releases are built
The Release workflow (.github/workflows/release.yml) publishes:
- the build of main, on each merge into main that changes the compiler,
the runtime or the tools: main holds the version being developed as a
prerelease (
0.4.0-devinVERSIONand each tool’sversion()), and the workflow publishes it on the prereleasev0.4.0-dev, moving its tag to the merge and replacing its archives. CI runs it on the merge before its tests, which then install that build. CI and the workers install the newest published toolchain, that prerelease or a release with a higher version, whateverVERSIONholds, instead of building the compiler. The newest release, the default, never picks a prerelease. - a release, on a
vX.Y.Ztag, made by hand. Its stage 0, the compiler that builds it, is the newest published toolchain too, so the release may take any version, not only the one main was developing:- A pull request runs
scripts/set-version.sh 0.9.0, which sets the version everywhere a tool reads it; the same pull request sets the VS Code extension’spackage.jsonandpackage-lock.jsonto match. - Tag that merge
v0.9.0and push the tag. The workflow checks that the commit holds that version, builds it and publishes the release; theDocsworkflow publishes the docs from the same tag, and once the release’s archives are up the workflow dispatchesVSIXon the tag, which builds the extension with them and publishes it asvsix-v<extension version>. - Once it is published, the workflow opens a pull request that starts
the next minor version on main (
0.10.0-dev) and merges itself once its checks pass, unless main has already moved on; a major version is started by hand. A pull request the workflow opens with its own token starts no workflows, so it is opened with theRELEASE_BUMP_TOKENsecret when that is set, and otherwise the workflow dispatches CI on its branch.
- A pull request runs
For each platform, scripts/release-build.sh builds the
tools, lays them out as above, archives them as
pepper-<version>-<target>.tar.gz (.zip on Windows) with a .sha256,
packs pep-install alone as the installer archive,
pepper-install-<version>-<target>.tar.gz (.zip) with its own .sha256,
and scripts/release-smoke-test.sh proves the archives work: unpacked outside the
repository with no LLVM variables set, pepper build must build and run a
program, and the binaries must load nothing beyond the system’s C and C++
runtimes (on Linux, nothing newer than glibc 2.28: the Linux leg builds in a
manylinux_2_28 container), and the installer’s pep-install must run. The workflow then installs each archive with
the install script. Last, it uploads the archives, their checksums, a
SHA256SUMS and both scripts to the release. A pull request that
changes these scripts runs every leg too, without publishing.
The pepper command
pepper is the entry point to every Pepper tool, the way go and git
are: pepper <command> [arguments] runs a separate program named
pep-<command> with the remaining arguments.
pepper build main.pep -o main # runs pep-build main.pep -o main
pepper build # builds the project pep.toml describes
pepper run hello.pep a b # builds hello.pep and runs it with a b
pepper fmt -w src # runs pep-fmt -w src
pepper -C my_app build # runs pep-build in my_app
pepper +0.2 build # runs the installed 0.2.x toolchain's pep-build
pepper help # lists the commands it can find
pepper help build # runs pep-build --help
pepper --version # pepper's version; -v adds each command's
pepper is itself written in Pepper (tools/pepper/main.pep). The compiler
is the program pep-build, so pepper build takes exactly the options the
compiler does (see The compiler: pep-build).
Usage and options
pepper [+<version>] [-C <dir>]... <command> [arguments]
pepper help [<command>]
pepper --version [-v]
| Form | What it does |
|---|---|
pepper <command> [arguments] | Runs pep-<command> with the arguments (see How it finds a command) |
+<version> | Runs the command with that installed toolchain, such as +0.3.0 or +0.3 (see Toolchains) |
-C <dir> | Changes to <dir> before running the command (see Running in another folder) |
pepper help, -h, --help | Prints the usage, these options, and the commands it can find |
pepper help <command> | Runs pep-<command> --help |
--version | Prints pepper and its version, and in a Pepper home the toolchain the folder uses and why |
--version -v, --version --verbose | Also prints the version each command reports (its --version’s first line, or (no version)) |
Options go before the command: everything after the command belongs to it,
so pepper fmt --help is pep-fmt --help. pepper with no arguments, or
with only -C and +<version> options, prints the usage to standard error and exits with
status 2; so does a -C with no folder after it; an option other than
-v or --verbose after --version is an error that exits with 2. An
unknown command exits with 1. Otherwise the exit status is the command’s.
The commands in this repository
| Command | Program | What it does |
|---|---|---|
pepper build | pep-build | Compiles a program, or with no file the project (The compiler: pep-build, Projects: pep.toml) |
pepper run | pep-run | Builds a program and runs it, like python script.py (Running a program: pep-run) |
pepper install | pep-install | Installs toolchains side by side and picks the default (Toolchains: pep-install) |
pepper package | pep-package | Adds, removes, updates and fetches a project’s dependencies (The package manager: pep-package) |
pepper fmt | pep-fmt | Formats source (Formatting with pep-fmt) |
pepper profile | pep-profile | Profiles a program and draws a flame graph (Profiling with pep-profile) |
pepper test | pep-test | Builds and runs the [Test] functions (Testing with pep-test) |
pepper lsp | pep-lsp | Runs the language server, which editors start (The language server: pep-lsp) |
pepper doc | pep-doc | Generates the library reference and builds this book (The documentation builder: pep-doc) |
Every one of them, pep-build included, is a Pepper program under tools/,
built with pep-build.
Building it
Build pepper with a pep-build (an installed one, or one you built from
tools/compiler; see
Building from source)
and put it next to it:
target/bin/pep-build tools/pepper/main.pep -o target/bin/pepper
On Windows, name the output target\bin\pepper.exe.
How it finds a command
Outside a Pepper home, pepper <command> looks for pep-<command> (pep-<command>.exe on
Windows) in the directory that holds the running pepper first, then in
each directory on the PATH, and runs the first it finds. So the tools
installed next to pepper win over others with the same name, and any
program named pep-<name> on the PATH adds the command <name>: a tool of
your own becomes pepper <name> once it is installed. On Linux and macOS
only files with an execute permission count.
The command takes pepper’s place, with its standard input, output and
error: on Linux and macOS pepper execs it, so signals such as Ctrl-C reach
the command directly and its exit status is pepper’s. Windows cannot
replace a process, so there pepper waits for the command, leaves Ctrl-C
to it, and exits with its exit status. A command name is lowercase letters, digits, - and _;
anything else, such as a path, is not a command.
An unknown command is an error that suggests the closest command and lists
the ones pepper found:
$ pepper buidl main.pep
pepper: error: 'buidl' is not a pepper command; did you mean 'build'?
Commands:
build
fmt
Running in another folder
-C <dir> runs the command as if pepper had been started in <dir>, like
git -C: pepper -C my_app build builds the project in my_app from
anywhere. Repeated, each -C is relative to the one before, so
pepper -C a -C b build runs in a/b; an empty -C "" changes nothing.
A folder that cannot be entered is an error that exits with status 1:
$ pepper -C nowhere build
pepper: error: cannot change to 'nowhere': No such file or directory
-C and +<version> are the only options that go before a command. They
work with help and the old form too.
Toolchains
Installed by pep-install, pepper lives in the Pepper
home’s bin folder (~/.pepper/bin, %LOCALAPPDATA%\pepper\bin on
Windows), beside the installed toolchains, and runs the tools of one of
them, or of a toolchain in a folder PEPPER_TOOLCHAIN_PATH lists (see
More toolchain folders). The first of these that names a toolchain picks it:
+<version>before the command:pepper +0.3.0 build;- the
PEPPER_TOOLCHAINenvironment variable; toolchain = "0.3"in[package]of the nearestpep.tomlat or above the folder the command runs in (after-C), so a project pins the compiler it builds with (see Projects: pep.toml);- the default, which
pepper install usesets.
A version is exact (0.3.0), or a prefix that picks the newest installed
toolchain that starts so (0.3, 0), or latest for the newest installed.
pepper then looks for pep-<command> in that toolchain’s bin folder
first, then beside pepper, then on the PATH, and runs it with
PEPPER_TOOLCHAIN set to the exact version, so a pepper that command runs
uses the same toolchain. pepper install always runs the home’s own
pep-install, which needs no toolchain.
When the toolchain picked is not installed, or none is installed, pepper
says so and how to install it, and exits with status 1:
$ pepper build
pepper: error: toolchain 0.4 (pinned by /work/app/pep.toml) is not installed; install it with 'pepper install add 0.4'
pepper install which shows which toolchain a folder uses and why.
Outside a Pepper home, as when it is built from this repository, pepper
runs the tools beside it, and +<version> is an error.
The old form
Before pepper build, pepper was the compiler. A first argument that is a
.pep file or an option still runs the compiler with the whole command line,
after a warning:
$ pepper main.pep -o main
pepper: warning: 'pepper main.pep ...' is deprecated; use 'pepper build main.pep ...'
The old form will be removed in a later release.
Toolchains: pep-install
pep-install installs Pepper toolchains side by side and picks the default
one, the way rustup and nvm do. pepper install runs it with the same
arguments:
pepper install list # the installed toolchains, marking the default and the one in use here
pepper install list --available # the releases with an archive for this machine
pepper install add 0.3 # installs the newest 0.3.x; the default stays as it is
pepper install use 0.3.0 # makes it the default
pepper install update # installs the newest release and makes it the default
pepper install add --force 0.3.0 # installs 0.3.0 again, replacing it
pepper install remove 0.2.1
pepper install which # the toolchain this folder uses, why, and its pep-build
pepper install setup # sets this machine up: installs, sets the default and the PATH, checks
pepper install uninstall # removes the home and its PATH changes
pep-install <command> [arguments]
A toolchain is one release of the compiler, its runtime and the tools
(pep-build, pep-fmt, pep-test, …). The pepper command
that pep-install sits beside runs the tools of the toolchain a folder
uses: the one pepper +<version> names, else PEPPER_TOOLCHAIN’s, else the
one the project’s pep.toml pins with toolchain = "0.3", else the default
(see Toolchains).
Options and commands
| Command or option | What it does |
|---|---|
list | Lists the installed toolchains, newest first, marking the default and the one the current folder uses, with why |
list --available, -a | Lists the releases that have an archive for this machine instead, marking those installed |
add <version> | Installs the newest release <version> names, unless it is installed; it becomes the default only when there was none |
add --force <version>, -f | Installs it even when it is installed, replacing the home’s copy |
use <version> | Makes the newest installed toolchain <version> names the default |
update | Installs the newest release, unless it is installed, and makes it the default; older toolchains stay installed. Then updates the home’s pepper and pep-install from it |
update --force, -f | Installs the newest release even when it is installed, replacing the home’s copy |
remove <version> | Removes the installed toolchain <version> names; it must name just one, and not the default |
which | Prints the toolchain the current folder uses and why, then the path of its pep-build; fails when that toolchain is not installed |
setup [<version>] | Sets this machine up for Pepper: installs the newest release <version> names (default latest), makes it the default, puts its pepper and pep-install in the home’s bin, adds that to the PATH, offers to install what linking needs, and builds and runs a program to check (see Setting a machine up) |
setup --archive <file> | Installs the downloaded release archive <file> instead, checked against <file>.sha256 beside it |
setup --yes, -y | Installs missing prerequisites without asking |
setup --no-modify-path | Leaves the PATH and the shell startup files alone |
setup --no-prerequisites | Neither checks for nor installs clang and the other prerequisites |
uninstall | Removes the Pepper home and the PATH changes setup made for it |
--path <dir> | Also finds toolchains in <dir>, before those PEPPER_TOOLCHAIN_PATH lists; repeatable, before or after the command (see More toolchain folders) |
-h, --help | Prints the commands; after a command, that command’s options |
-V, --version | Prints the version |
A <version> is exact (0.3.0), or a prefix (0.3, 0) for the newest
release, or installed toolchain, that starts so, or latest for the newest.
A prerelease such as 0.4.0-dev, the build of main that each merge
replaces, is picked only by naming it exactly; prefixes, latest and
update leave prereleases out.
Only releases are installed; there is no nightly channel.
The exit status is 0 on success, 1 when a command fails, and 2 for a mistake on the command line.
Where toolchains go
The Pepper home holds them:
~/.pepper/bin/pepper the entry point
~/.pepper/bin/pep-install
~/.pepper/toolchains/0.3.0/bin/ a toolchain's pep-build, pep-fmt, ...
~/.pepper/toolchains/0.3.0/runtime/ and its runtime
~/.pepper/default-toolchain the default toolchain's version
The install scripts set up the home by running setup (see
Setting a machine up): it installs the newest
release this way and puts its pepper and pep-install in bin.
update replaces the home’s pepper and pep-install with the new
toolchain’s when they differ. Each is written beside the old one and
renamed into place, the old one first moved aside to <name>.old; on
Windows, where a running program cannot be removed, that .old file is
removed by the next update.
add --force and update --force install a toolchain that is already
installed again, the way a fresh one is: downloaded, checked and unpacked
beside its folder. The old folder is then moved aside, the new one put in
its place and the old one removed, so no stale files remain, and a failed
download or unpack leaves the old toolchain as it was. On Windows a
toolchain one of whose programs is running (a pep-lsp the editor
started, say) cannot be moved, and the install fails, leaving it in place.
The home is the folder above the running pep-install when that is a home
(it has a toolchains folder), else PEPPER_HOME, else
%LOCALAPPDATA%\pepper on Windows and ~/.pepper elsewhere.
Setting a machine up
setup is what the install scripts run, from the installer
archive each release carries (pepper-install-<version>-<target>.tar.gz,
.zip on Windows, holding only pep-install). It does what add and
use do, then:
- puts the toolchain’s
pepperandpep-installin the home’sbin, asupdatedoes; - unless that folder is on the PATH already or
--no-modify-pathis given, adds it: on Windows to the user’s PATH, elsewhere with a line in~/.profile, and in~/.bashrc,~/.bash_profileand~/.zshrcwhen they exist (~/.zshrcalso when the shell is zsh), and in fish’sconf.d/pepper.fishwhen fish is installed. Each line follows a# Added by the Pepper installercomment; - unless
--no-prerequisitesis given, checks for what linking a program needs and offers to install what is missing, asking on the terminal first unless--yesis given: Xcode’s command line tools on macOS, the Visual Studio Build Tools’ C++ workload on Windows (with winget), and clang when the check program’s build finds none (apt, dnf, yum, pacman or zypper on Linux, through sudo when not root; winget or Scoop on Windows). It notes when OpenSSL 3, which thetlsmodule loads, is missing, and on macOS offersbrew install openssl@3; - builds and runs a small program with the home’s
pepper, from an empty folder, and fails when it cannot.
A toolchain from before side-by-side toolchains, which has no pepper to
put in bin, cannot be set up. An archive named for no release (a dev
build, pepper-dev-<target>) is installed under the version its
pep-build reports.
uninstall removes the home, refusing a folder that holds neither
toolchains nor bin/pep-build, and the PATH lines setup added for that
home’s bin.
More toolchain folders
Toolchains are also found in the folders PEPPER_TOOLCHAIN_PATH lists,
separated as PATH separates them (; on Windows, : elsewhere), and for
pep-install in each --path folder, which come first. Each is laid out
as the home’s toolchains folder is, a folder per toolchain named by its
version:
/opt/pepper/0.3.0/bin/pep-build
/opt/pepper/0.3.0/runtime/
PEPPER_TOOLCHAIN_PATH=/opt/pepper pepper +0.3.0 build main.pep
pepper install --path /opt/pepper use 0.3.0
The home’s own toolchains folder is searched first, so a toolchain there
wins over one of the same version elsewhere. list marks a toolchain found
elsewhere with its folder; use, which and pepper +<version> take it
as they take one in the home, and add and update count it as
installed (--force installs into the home all the same). remove removes
only toolchains in the home: a toolchain in another folder is left to
whoever put it there. A project’s pep.toml cannot add folders, since
where toolchains are differs from one machine to the next.
Editors
The VS Code extension runs the language server of the toolchain the
workspace uses, as pepper install which reports it, and offers to
install Pepper, the toolchain the workspace needs, or a newer default when
they are missing or old; see pep-lsp.
Downloads
add and update find this machine’s archive (linux-x64, macos-arm64,
macos-x64 or windows-x64) among the GitHub releases, as the
install scripts do, download it and check it against its
.sha256. The archive is unpacked beside the toolchain’s folder first, so
a download that fails or does not match its checksum leaves nothing behind.
| Variable | What it does |
|---|---|
PEPPER_HOME | Where toolchains go, when pep-install does not run from a home |
PEPPER_TOOLCHAIN_PATH | More folders to find toolchains in (see More toolchain folders) |
PEPPER_INSTALL_RELEASES_URL | Lists the releases from this URL instead of GitHub’s API, for a mirror; it serves the same JSON |
GITHUB_TOKEN, GH_TOKEN | A token sent to GitHub’s API while the repository is private, and to nothing else |
The compiler: pep-build
pep-build compiles a Pepper program into an executable, or into a library
for C code to link. pepper build runs it with the same arguments, so
everything on this page applies to both spellings:
pepper build main.pep -o main # the same as: pep-build main.pep -o main
pep-build [OPTIONS] [<entry.pep>]
This page lists every option. pep-build --help prints the same list in
short, and pepper help build runs it.
All options at a glance
| Option | What it does | Default |
|---|---|---|
<entry.pep> | The program’s entry file | the project’s, from pep.toml |
-I <dir> | Add a folder to search for imported modules | the runtime’s folder only |
--import <name> | Load a module into every file of the program | none |
--entry <name> | The function the program starts in, instead of main; it takes no parameters or one String[], and every main is left out (not with --test or --lib) | main |
--no-runtime | Compile without the standard library | the runtime is used |
-o <file> | Name the executable or library (with --emit-static, the folder) | <entry stem> in the current folder; a project’s name in its folder |
--lib static, --lib shared | Build a library for C instead of an executable | an executable |
--test | Build the [Test] functions of the entry file (or of several files of one folder) into a test program | off |
--build <dir> | Folder for intermediate files | .pep/build (in a project’s folder when building it) |
--emit-llvm | Write LLVM IR instead of an executable | off |
--emit-static, --emit-static=keep | Write the source with each static block expanded, instead of compiling | off |
--emit-ast[=text|json] | Print the syntax tree instead of compiling | off |
--emit-linked | Print every file’s syntax tree after linking, as JSON, instead of compiling | off |
--emit-checked | Print what the checks after linking report, instead of compiling | off |
--emit-model | Print the program’s sema model before any sema pass, as JSON, instead of compiling | off |
--emit-model-step <N> | With --emit-model, print the model after the first N sema steps | 0 |
--emit-steps | Print the names of the sema steps, one per line, instead of compiling | off |
-O, -O0 to -O3 | Optimization level | -O0, or PEPPER_OPT_LEVEL |
--lto=full, --lto=off | Link-time optimization | off |
-march=, -mcpu=, -mtune=, -m<feature>, -mno-<feature> (-m <option>) | The CPU to generate code for | clang’s default for the target |
-g, --emit-debug | Emit debug info | off |
--debug-static=source, --debug-static=expanded | With -g, step through static blocks as written or expanded | source |
--frame-pointers | Keep every function’s frame pointer, for profilers | off |
--target <triple> | Compile for another platform | the machine compiling |
--sysroot <dir> | The target’s system libraries | PEPPER_SYSROOT_<TRIPLE> |
-l <lib> | Link a C library | the platform’s defaults |
-L <dir> | Add a folder the linker searches | PEPPER_LIBRARY_PATH |
--MSVC | Use the MSVC linker (not ported) | off |
-D <name>[=<value>] | Define a build option | none |
--no-runtime-checks=<list> | Remove kinds of runtime check | every check on |
--no-cache | Do not use the build cache | the cache is used |
--cache <dir> | Folder for the build cache | PEPPER_CACHE_DIR, else .pep/cache (in a project’s folder when building it) |
--verify-cache | Check cache hits against fresh code | off, or PEPPER_CACHE_VERIFY |
--no-verify-cache | Do not check cache hits | |
-T <channel>, --Tall | Print timings and cache decisions | off |
-v, --verbose | Accepted; prints nothing more yet | off |
--compile-part <dir>, --part <N>=<object>, --compilation-unit-worker <dir> | Internal: what a build runs in the children it starts | |
-h, --help | Print the options | |
-V, --version | Print the version |
An option that takes a value accepts it as the next argument or attached:
-I lib, -Ilib, --target aarch64-apple-darwin and
--target=aarch64-apple-darwin are all the same. -O, --emit-ast and
--emit-static take a value only attached (-O3, --emit-ast=json), so
the argument after them is never read as their value. Short options that
take no value combine (-gv is -g -v). Options may come before or after
the entry file, and every argument after -- is a file. An unknown option is
an error, and the compiler exits with status 2 after a short usage message.
Inputs and imports
The entry file
pepper build main.pep
The file that holds main. The compiler finds the files it imports on its
own (see Modules and imports), so a program of many
files is compiled by naming its entry file alone. Only the first file named
is compiled; any others are ignored.
With no file, the compiler builds the project whose pep.toml is in the
current folder or the nearest folder above it: its [build] table names the
entry file and the output, and the build goes in the project’s .pep (see
Projects: pep.toml). With no file and no pep.toml, it stops
with error: no source files provided, and no pep.toml in <folder> or a folder above it.
cd my_app/src
pepper build # builds my_app/pep.toml's program into my_app/
An entry file inside a project (under a folder with a pep.toml) imports the
project’s dependencies, and its [build] table’s include folders and
link libraries apply to it as well.
Before compiling a program in a project, the compiler fetches whatever
pep.lock lists and the project’s .pep/deps lacks or holds at another
commit, and the dependencies pep.toml names that pep.lock does not list
yet, by running pep-package fetch (beside pep-build, else on the
PATH). Without
pep-package, it checks the missing commits out with git itself, and
asks for pepper package fetch to move a checkout at another commit. A
dependency checked out on a branch is left as it is. See
The package manager: pep-package.
-I <dir>
Adds a folder to search for modules imported by name (import geometry;
looks for geometry/module.pep). The -I folders are searched in the order
given, then a project’s [build] include folders, then the runtime’s
folder. A project’s dependencies are found through its pep.toml instead.
See
Dependencies and search paths.
pepper build tools/pep-fmt/main.pep -o pep-fmt
--import <name>
Loads a module (<name>/module.pep) or a file (<name>.pep), found on the
search paths, into every file of the program, the way the standard library’s
prelude is loaded. Its definitions come ahead of the runtime’s, so one that
redefines a runtime export wins. Repeat it for more modules. A name that
cannot be found is an error that lists the folders searched. See
The runtime prelude and --import.
--no-runtime
Compiles without the standard library: the runtime is neither looked for
nor loaded, so only the program’s own files and what -I reaches are
compiled. For testing the compiler; ordinary programs need the runtime
(print, String and Array all live there).
Without this option the compiler finds the runtime as
Getting started
describes, and PEPPER_RUNTIME names its folder outright.
Output
-o <file>
The path of the executable, or with --lib of the library. Without it, the
output goes to the current folder, named after the entry file with no
extension (main for main.pep); with --lib, the library’s usual file
name for that stem (libmain.a). Building a project with no file, it is the
[build] table’s output, else the package’s name in the project’s
folder (libname.a with --lib), with .exe added when building for
Windows and the name has no extension. Windows runs only files ending in
.exe, so otherwise name the output there: -o main.exe.
With --emit-static, it is the folder the expanded source goes into.
--lib static, --lib shared
Builds a library for C code to link instead of an executable, with a C
header beside it that declares the program’s [Export]ed functions. No
main is needed.
--lib staticwrites an archive:libname.a(name.libon Windows), holding the objects of the whole program, the runtime’s included, with the index of their symbols that linkers search. The compiler writes it itself in the target’s format (GNU’s for Linux and Android, the BSD format Apple’s linkers read for macOS and iOS, the COFF archive for Windows), so no archiver needs to be installed, and building one for another platform needs no system root: nothing is linked. A program linking it also links the libraries the program does, which the header names in a comment (/* Link the static library with: -lm */).--lib sharedlinks a shared library:libname.so,libname.dylibon macOS,name.dll(and its import libraryname.lib) on Windows. Itssonameis its file name; on macOS and iOS its install name is@rpath/<file name>, so a program that links it finds it on its run path. Linking one for another platform needs its system root, as an executable does (--sysroot).
The header goes beside the library, named for it without the lib prefix
(libname.a has name.h). A library that exports nothing is built with a
warning. See
Calling Pepper from C.
--test
Builds the entry file’s tests into a test program, which
pep-test runs. A build without it leaves every [Test]
function out. With it:
- the
[Test]functions are kept, and the build optionTESTis defined (as by-D TEST), for code only tests use ([BuildOption("TEST")]); - the runtime’s
testingmodule is loaded into every file (as by--import testing); - the entry file’s
mainis replaced by one that answers--list(one line per test: name,file:line,returnsorpanics, and the text a[ShouldPanic]panic must contain, separated by tabs) and--run NAME [--timeout SECONDS](run that test; a test still running after the timeout ends the program with exit status 124).
Only the entry file’s [Test] functions become tests. Given several files
of one folder (pep-build --test a_test.pep b_test.pep), it builds the
tests of them all into one program: a generated entry file in that folder
(_pep_test_main.pep, never written to disk) imports each file and
registers its tests, and each file keeps the scope it has in any program.
--list then gives each test’s own file, and --run also takes
FILE::NAME, as --list spells the file, to tell apart tests of one name
in two files. It cannot be combined with --lib. See Unit tests.
--build <dir>
The folder for intermediate files: the program’s objects and, with
--emit-llvm, its IR. The default is .pep/build in the current folder, or
in the project’s folder when building a project with no file.
--emit-llvm
Writes the whole program’s LLVM IR to <build>/<name>.ll (.pep/build/main.ll
for main.pep), optimized at the -O level, and stops there: nothing is
linked and no executable is written.
pepper build main.pep -O --emit-llvm # .pep/build/main.ll
--emit-static, --emit-static=keep
Writes a copy of the program’s source with each static block replaced by
the code it emits, like a C compiler’s -E, and stops there: nothing is
compiled to code. Use it to see what a reflection loop, a serializer or a
library’s block does to the program (see
Seeing what a static block emits).
pepper build main.pep --emit-static # main.static/main.pep
pepper build main.pep --emit-static -o expanded # expanded/main.pep
pepper build expanded/main.pep -o main # the same program
- Every file of the program in the entry file’s folder (and the folders under it) is written into the output folder at the same relative path, so the folder builds as a whole program; files with no block are copied unchanged. The runtime and libraries found elsewhere are not copied. The originals are never overwritten.
- The folder is
-o’s, else<entry stem>.staticin the current folder. - Each expansion is a
{ ... }block between# static (out, value) expanded from main.pep:12:5and# end static.--emit-static=keepalso keeps the block as written, as#comments above it;--emit-static=expandis the default, the same as--emit-static. - A block in a generic function expands once per specialization. The
generic function stays as written, and after it comes a
concrete override
for each type the program uses it with (
describe<Point>(value: Point)), its blocks expanded, so the copy still builds into the same program. A block in a method of a generic struct, or in a generic method, stays, with each specialization’s expansion above it as#comments. - When
pep-fmtis next to the compiler or onPATH, it formats the copies, wrapping long lines. - Errors a block reports stop it, as they stop a build. Errors in the code a block emits are found when the copy is built, at the copy’s line.
--emit-ast
Parses the entry file only and prints its syntax tree to standard output, instead of compiling. Imports are not followed and nothing is checked.
--emit-ast and --emit-ast=text print the tree as source-like text.
--emit-ast=json prints it as one line of JSON: each node an object with
its fields in declaration order, each enum case an object with the case’s
name as its one key, and each span {"start": ..., "end": ...} in bytes.
The compiler’s parser written in Pepper (tools/compiler) prints the same
JSON, and its test compares the two over every source file in the
repository.
--emit-linked
Loads the program as a build does (the entry file, every file its imports reach, and the runtime), links it, and prints it to standard output as one line of JSON instead of compiling. Nothing after linking is checked, so a program with type errors prints too; a file that cannot be read or parsed stops it, as it stops a build.
The JSON is an object with three keys:
files: every file in load order (the entry file first), each with itsnameas the program reached it, itsnamespace, itsmodule_rootfolder (ornull), whether it is part of theprelude(the runtime and--importfiles) or aprelude_part(another file of a forced module), the index of the file each of itsimportsnames (ornullwhen one did not resolve), which of them arefolder_imports(a local import of a module folder), and itsmodule, the syntax tree as--emit-ast=jsonprints it after linking renamed what the file imports and gave its private declarations their program-wide names (helper@io/file).errors: the linking errors, by file and position, each with itsfileindex,span,message, whether it is anunresolvedimport, and itsnote([span, message], ornull).overload_unions: each overload set that spans files, by name, with its functions grouped by module.
-I, --import, --no-runtime, -D and --test apply as for a build.
The compiler’s linker written in Pepper (tools/compiler) prints the same
JSON, and its test compares the two.
--emit-checked
Loads and links the program as --emit-linked does, runs the checks that
come before type checking, and prints what they report instead of
compiling: the errors, rendered as a build reports them, then the warnings.
The errors are the linking errors; when there are none, a [__BuiltIn]
declaration outside the runtime; when there is none of those either, every
use of a declaration another file keeps private or this file does not
import. The warnings are deprecated syntax, comparisons with a build option
-D does not define, and uses of declarations marked [Deprecated]. A
program with neither prints an empty line.
-I, --import, --no-runtime, -D and --test apply as for a build.
The compiler written in Pepper (tools/compiler) prints the same text, and
its test compares the two.
--emit-model
Loads, links and checks the program as --emit-checked does, builds the
sema model of each file and joins them, and prints the model as one line of
JSON instead of compiling: what the sema builder made of the syntax trees
(for, with, match, ?, destructuring, ranges, collection literals and
properties lowered), before any sema pass runs. Object keys are sorted, and
so are the sets (private_functions and the extension_scopes). When the
checks or the builder reject the program, it prints the rendered error and
fails.
-I, --import, --no-runtime, -D and --test apply as for a build.
The compiler written in Pepper (tools/compiler) prints the same JSON, and
its test compares the two.
--emit-model-step <N>
With --emit-model, runs the first N steps of the sema passes before
printing the model. A step is one pass run, in the order the compiler runs
them (platform filters first, then [Equatable], closure lifting and the
rest); --emit-steps lists them. When a step rejects the program, it prints
the rendered error and fails. The compiler written in Pepper runs the same
steps, and its test compares the model after each.
--emit-steps
Loads, links, checks and builds the program as --emit-model does, runs
every sema pass, and prints the name of each step run, one per line,
instead of compiling: the numbering --emit-model-step uses.
Optimization and code generation
-O, -O<level>
The optimization level: -O0 (no optimization, the default), -O1, -O2
or -O3. -O alone is -O2, and it does not take the next argument as its
level, so pepper build -O main.pep works. Each level runs the pipeline
clang runs at that level. Without the option, the level is
PEPPER_OPT_LEVEL (0 to 3) when that is set and not empty.
At -O0 a panic prints a backtrace after its message; at any other level it
prints the message alone, unless the program runs with PEPPER_BACKTRACE=1
(see Backtraces on panic).
--lto=full, --lto=off
Link-time optimization. With --lto=full, once every file is compiled the
whole program is optimized again as one module, so a call into another file
(the runtime included) can be inlined. --lto=off optimizes each file
alone, which links sooner. The default is off: full optimizes the whole
program again on every build, an edit’s too, so it suits a release build.
--lto=full at -O0 is an error that asks for -O.
With it, each file is optimized by LLVM’s pre-link pipeline and kept as
bitcode (in the build cache, as an object is), then the compiler links
them into one module, makes every symbol internal but main and the C
exports of [Export] functions, runs LLVM’s link-time pipeline over the
whole program, and writes its objects, one per logical CPU in parallel,
which link as usual. No linker
plugin is involved, so it works on every target. It applies to a library
too (--lib).
-march=<cpu>, -mcpu=<cpu>, -mtune=<cpu>, -m<feature>, -mno-<feature>
The -m options choose the CPU the code is generated for, with clang’s
names and meanings (-m <option>, with a space, is the same as
-m<option>):
- On x86-64,
-march=<cpu>names the CPU:x86-64-v3,znver4, ornativefor the machine compiling. - On AArch64,
-mcpu=<cpu>names the CPU (apple-m2,native) and-march=<arch>the architecture (armv8.2-a+crypto). -mtune=<cpu>changes only how instructions are scheduled.-m<feature>adds a CPU feature (-mavx2), and-mno-<feature>removes one (-mno-avx2).
Without them the program runs on any machine of its kind: clang’s default
CPU for the target (x86-64; apple-m1 on Apple arm64). An option the
target’s architecture does not take is an error worded as clang words it:
-mcpu= on x86-64 (unsupported option '-mcpu=' for target ...; use '-march' or '-mtune'), -march= on WebAssembly, an AArch64 -march=
that is not armv<version>-a (invalid arch name), native for another
platform’s target, or an empty value. A CPU or feature name LLVM does not
know is passed on, and LLVM warns about it. The last of each option wins;
the features come after those the CPU implies. The chosen CPU, its
features and its tuning are part of the build cache’s key.
-g, --emit-debug
Emits debug info for gdb, lldb, Visual Studio and WinDbg: DWARF 4 in the
executable on Linux and macOS, and CodeView on Windows, where the linker
writes <output>.pdb beside the executable. On macOS it also runs
dsymutil to write <output>.dSYM beside the executable, when the
program is built for the Mac it runs on. It works with -O. See
Debugging.
A Windows build without -g also writes <output>.pdb, holding only the
program’s function names, which is where backtraces
read them.
--debug-static=source, --debug-static=expanded
With -g, chooses which source the debugger shows for the code a static
block emits (see
Stepping through a static block):
source, the default: the block as written. Stepping stops on a compile-time loop’s header once per pass, then on its body.expanded: a copy of each file with its blocks expanded, as--emit-staticwrites it (unformatted), saved beside the output as<name>.static.pep(main.static.pepformain.pep; a file in a folder under the entry’s keeps that folder). The debugger steps through the expanded statements,sum = sum + p.y;, and a breakpoint on one of its lines stops in that pass. The rest of the program stays in its own files. A build with it does not use the build cache.
pepper build -g --debug-static=expanded main.pep -o main # main, main.static.pep
--frame-pointers
Keeps the frame pointer in every function, even when optimizing, as clang’s
-fno-omit-frame-pointer does. A sampling profiler then walks the stack of
each sample by following frame pointers, which is fast and finds every
frame of Pepper code; without them, optimized code reuses the register and
a profiler loses the callers of whatever was running. It costs a register
and a few instructions per call. pep-profile builds with
it, -O and -g. It is part of the build cache’s key.
Targets and cross-compiling
--target <triple>
Compiles for another platform, named by an LLVM target triple:
x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu,
aarch64-apple-darwin, x86_64-apple-darwin, x86_64-pc-windows-msvc,
aarch64-pc-windows-msvc, wasm32-wasip1, aarch64-apple-ios,
aarch64-apple-ios-simulator, x86_64-apple-ios-simulator,
aarch64-linux-android or x86_64-linux-android, with a version allowed
where clang allows one (arm64-apple-macosx14.0, aarch64-apple-ios17.0,
aarch64-linux-android33). An unsupported triple is an error that lists the
supported ones. [Target(...)] selects code for the target. The default is
the machine the compiler runs on. See
Cross-compiling and
iOS and Android.
--sysroot <dir>
The folder that holds the target’s system headers and libraries, passed to
the link driver, for linking with --target. Without it, the folder named by
PEPPER_SYSROOT_<TRIPLE> when that is set: the triple in capitals, with _
for each character that is not a letter or digit
(PEPPER_SYSROOT_AARCH64_APPLE_DARWIN for aarch64-apple-darwin). An iOS
or Android triple’s variable leaves out the OS version
(PEPPER_SYSROOT_AARCH64_APPLE_IOS_SIMULATOR for
aarch64-apple-ios17.0-simulator). For iOS it is an iOS SDK, which on a Mac
defaults to the one xcrun names; for Android it is the NDK (see
ANDROID_NDK_HOME below).
Linking
-l <lib>
Links a C library, as -l does for clang: -l m links libm. Repeat it
for more libraries. A project’s [build] table adds its link libraries. A program’s [Link] attributes name libraries from the
source instead. See Linking.
-L <dir>
Adds a folder the linker searches for libraries. Folders given with -L
come first, then those of [Link(path = ...)], then those in
PEPPER_LIBRARY_PATH (separated like PATH: :, or ; on Windows). See
Library search directories.
pepper build main.pep -L libs -l foo # links libs/libfoo
--MSVC
Reserved for linking with Microsoft’s linker. It is not ported, so it is an error on every platform; Windows executables link with clang and lld.
The link driver is clang. When PEPPER_LLVM_PREFIX, else
LLVM_SYS_<major>1_PREFIX (for the LLVM the compiler was built with, such
as LLVM_SYS_201_PREFIX for LLVM 20), names an LLVM install, its
bin/clang and lld are used; else a clang beside
pep-build, then one on the PATH (clang, then clang-<major>), then one
where LLVM’s packages install it off the PATH. With none, the link fails
with an error naming what to install. Installing Pepper
lists the folders searched.
Build options
-D <name>, -D <name>=<value>
Defines a build option. A bare name is name=1, as in C. [BuildOption]
and [Target(BuildOption(...))] select code by it, and static blocks read
it with reflect.build_option. Repeat it for more options; of two that name
the same option, the later wins. A name must be an identifier
(-D 2FAST is an error). Build options are part of the build cache’s key.
See Build options.
pepper build main.pep -D USE_ZLIB -D LOG_LEVEL=2
Runtime checks
--no-runtime-checks=<list>
Integer overflow, division by zero, an oversized shift, an index out of range and a call through a null reference panic at run time, at every optimization level. This option removes the kinds of check it names, as a comma-separated list, from an optimized build:
| Kind | What is no longer checked | What happens instead |
|---|---|---|
overflow | Integer overflow | Arithmetic wraps |
divide | Division by zero and MIN / -1 | Undefined behavior |
shift | A shift count of at least the width | The count is masked |
bounds | Array and string indexing | Undefined behavior |
null | Calls and matches through a null reference | Undefined behavior |
all | All of the above |
Removing bounds or null gives up memory safety. At -O0 the option has
no effect and prints warning: '--no-runtime-checks' has no effect at -O0.
To remove checks from one function at every level, mark it
[Unsafe("overflow,bounds")] instead.
See Integer overflow.
pepper build -O main.pep --no-runtime-checks=overflow,shift
The build cache
The compiler checks a program one compilation unit at a time: a file,
or the files that import each other in a cycle, with the runtime as one.
The build cache keeps each compilation unit’s checked result and object,
and each file’s own object: a file whose sources, and whose imports’
declarations, did not change is not compiled or checked again.
PEPC_COMPILATION_UNIT_JOBS sets how many compilation units are checked at
once (one per processor by default; PEPC_LAYER_JOBS, its old name, still
works). The optimization level, link-time optimization, debug info,
CPU, removed runtime checks and build options are part of each object’s
key. Deleting the cache folder is always safe.
--no-cache
Compiles every file without reading or writing the build cache.
--cache <dir>
Keeps the build cache in <dir>. Without it, the folder named by
PEPPER_CACHE_DIR when that is set and not empty, else .pep/cache in the
current folder, or in the project’s folder when building a project with no
file.
--verify-cache, --no-verify-cache
--verify-cache checks the cache, for testing the compiler: every file the
cache would reuse is compiled anyway, and the build fails if its code
differs from the cached object’s, or if skipping the reused files’ bodies
changes another file’s code. Setting PEPPER_CACHE_VERIFY to anything but
empty or 0 does the same. --no-verify-cache turns it off even when
PEPPER_CACHE_VERIFY is set. The two options cannot be combined.
It builds the program a second time, with an empty cache of its own in the
build folder, and compares each object that build makes with the one the
cache holds under the same key, so a check costs about a build from
scratch. A failure names the cached objects that differ.
Tracing and diagnostics
-T <channel>, --Tall
-T turns on a trace channel, which prints to standard error. Repeat it for
more channels; --Tall turns on every one.
-T timeprints how long each stage of the compile took (time: check and build 470.763 ms).-T passesprints how long each pass of the checker took, summed over its runs, slowest first (pass: 3116.171 ms 16x resolve_implicit_types), the passes run checking the runtime and each module included.-T cacheprints how the build cache supplied each object:hit(its sources, and its imports’ declarations, are unchanged),hit (same code)(compiled again, to the same code), ormiss; and whether linking was skipped.
Any other channel name is accepted and ignored.
-v, --verbose
Accepted for compatibility with other compilers; it prints nothing more yet.
Errors and warnings are printed like clang’s, colored when standard error is
a terminal; set NO_COLOR to turn color off. See
Compiler errors.
Internal options
A build starts copies of the compiler to work side by side, and gives them these options; they are listed so a failed child’s command line can be rerun by hand (the build prints it).
--compile-part <dir> --part <N>=<object>...compiles parts of the program a build split into<dir>: each--partnames a part by its number and the object to write it to. Nothing else is read.--compilation-unit-worker <dir>, after the build’s own arguments, checks the compilation units the build passes through<dir>and compiles their objects.
Help and version
-h,--helpprints every option with a line of description.-V,--versionprintspep-buildand its version.
Environment variables
| Variable | What it does | Option that overrides it |
|---|---|---|
PEPPER_RUNTIME | The folder holding the standard library; nothing else is searched | --no-runtime skips the runtime |
PEPPER_OPT_LEVEL | The optimization level (0 to 3) without -O; also the level the test suite’s JIT uses | -O<level> |
PEPPER_LIBRARY_PATH | More folders the linker searches, after -L and [Link(path = ...)] | -L comes first |
PEPPER_SYSROOT_<TRIPLE> | The system libraries for linking with --target <triple> | --sysroot |
ANDROID_NDK_HOME, ANDROID_NDK_ROOT | The Android NDK for linking with an Android --target, when neither --sysroot nor PEPPER_SYSROOT_<TRIPLE> names one | --sysroot |
ANDROID_HOME, ANDROID_SDK_ROOT | The Android SDK, whose newest ndk/<version> links an Android --target when nothing above names an NDK | --sysroot |
WASI_SDK_PATH | The wasi-sdk for linking with --target wasm32-wasip1, when neither --sysroot nor PEPPER_SYSROOT_WASM32_WASIP1 names one | --sysroot |
PEPPER_CACHE_DIR | The build cache folder | --cache, --no-cache |
PEPPER_CACHE_VERIFY | Anything but empty or 0 checks the build cache | --no-verify-cache |
PEPPER_LLVM_PREFIX | The LLVM install whose clang and lld link the program | |
LLVM_SYS_<major>1_PREFIX | The same, when PEPPER_LLVM_PREFIX is not set (LLVM_SYS_201_PREFIX for LLVM 20) | |
PEPPER_PACKAGE_GITHUB_URL, PEPPER_PACKAGE_GITLAB_URL, PEPPER_PACKAGE_CODEBERG_URL | Where a build fetching a project’s locked dependencies clones that host’s repositories from, when pep-package is not installed (see pep-package) | |
NO_COLOR | Set and not empty: errors are not colored |
Exit status
pep-build exits with 0 when the program compiled, 1 when it did not (the
errors are on standard error), and 2 for a command line it cannot parse. A
crash of the compiler itself exits with 101 after printing what went wrong;
please report it.
Projects: pep.toml
A pep.toml file marks the root of a project. It names the project, says
how pepper build builds it, and lists the packages it depends on, so a
project builds with pepper build and nothing else:
cd my_app
pepper build # builds the program pep.toml describes
./my_app
[package]
name = "my_app"
version = "0.1.0"
description = "Draws shapes"
authors = ["Ada <ada@example.com>"]
license = "MIT"
repository = "https://github.com/me/my_app"
[build]
entry = "src/main.pep"
[dependencies]
geometry = { github = "someone/geometry", version = "1.2" }
json = { gitlab = "group/pepper-json", version = "0.4.x" }
fast_json = { github = "other/json", version = "2" } # renamed: 'json' was taken
http = { codeberg = "someone/http", branch = "main" }
shapes = { path = "../shapes" }
A package is a project other projects depend on: a repository (or folder)
with a pep.toml at its root. Its root module.pep is the module its
dependency name imports, and any folder inside it holding a module.pep is a
module of the package too.
[package]
| Key | What it is |
|---|---|
name | The project’s name: letters, digits and _, not starting with a digit. Required. pepper build names the executable after it. |
version | Its version, major.minor.patch |
description | One line saying what it is |
authors | An array of names |
license | Its license, such as MIT |
repository | Where its source lives |
toolchain | The toolchain it builds with: 0.3.0, 0.3 for the newest installed 0.3.x, or latest. A pepper installed by pep-install runs that toolchain’s tools in the project (see Toolchains); the compiler itself only checks that it is a version |
[build]
pepper build with no file builds the project: it finds the nearest
pep.toml in the current folder or a folder above it, and builds the
[build] table’s entry file into its output. Every path is relative to the
folder holding pep.toml, and the build’s intermediate files and cache go in
that folder’s .pep, wherever pepper build runs.
| Key | What it is | Default |
|---|---|---|
entry | The file that holds main | main.pep |
output | The executable; building for Windows adds .exe to a name with no extension | the package’s name |
include | An array of extra folders to search for modules, as -I adds | none |
link | An array of C libraries to link, as -l adds | none |
Options on the command line still apply: pepper build -O builds the
project optimized, and -o names another output. A file argument builds that
file instead (pepper build tools/gen.pep); the project’s dependencies,
include folders and link libraries still apply to it, since it is in the
project.
[dependencies]
Each key is the name source code imports, and its value says where the package comes from and which revision to use:
[dependencies]
geometry = { github = "someone/geometry", version = "1.2" }
import geometry;
import geometry.solids; # the module in the package's solids/ folder
main(): int {
print("{0} {1}", geometry.area(3, 4), solids.volume(2));
return 0;
}
| Key | What it is |
|---|---|
github, gitlab, codeberg | The repository, owner/name (on GitLab, group/subgroup/name too) |
path | A folder on disk, relative to this pep.toml, instead of a repository |
version | A semver range of the repository’s version tags: 1.2, 0.4.x, >=1.0, <2 |
branch | A branch |
tag | A tag |
commit | A commit |
A dependency has exactly one source, and at most one of version, branch,
tag and commit; with none, it follows the repository’s default branch.
A path dependency takes none of them.
Imports
import geometry imports the package’s root module, its module.pep.
import geometry.solids imports the module in its solids folder, and binds
the last name, so its declarations are solids.volume; as names it
otherwise (import geometry.solids as s). A dotted import whose first name
is not a dependency looks for the nested folder on the search paths:
import a.b finds a/b/module.pep.
Because the name lives only in pep.toml, a package that moves to another
host or organization, or a switch to a fork, changes one line of pep.toml
and no source file. Two packages whose repositories share a name get
different keys:
json = { gitlab = "group/pepper-json" }
fast_json = { github = "other/json" }
Where dependencies live
A dependency from a Git host lives in the project’s
.pep/deps/<host>/<owner>/<name>, such as
.pep/deps/github.com/someone/geometry. One copy serves the whole project:
each package’s own pep.toml names the packages it needs under its own
keys, and an import in one of its files resolves against those keys, to the
same copies.
pepper package fetches them there and records the commit of each, the
dependencies’ dependencies included, in pep.lock beside pep.toml; commit
pep.lock with pep.toml. pepper build fetches what pep.lock lists and
.pep/deps lacks or holds at another commit, so a fresh clone builds with
pepper build alone, with git or, where git is missing, by downloading each
locked commit’s archive over HTTPS and checking it against the checksum
pep.lock records. See
The package manager: pep-package, which also says how
versions are chosen when several packages need one.
pepper package add someone/geometry # adds it to pep.toml and fetches it
pepper build
[fetch]
[fetch]
hosts = ["codeberg", "github"]
| Key | What it is | Default |
|---|---|---|
hosts | The hosts pepper package add owner/name looks a repository up on, in order, when the argument names none; it adds the first that has it | ["github", "gitlab", "codeberg"] |
Errors
A problem in pep.toml is an error located at the key or value to change:
pep.toml:9:1: error: dependency 'io' has the name of the runtime module 'io'; give it another name, which source code then imports
9 | io = { github = "someone/io" }
| ^~
- Two dependencies with the same name, or a dependency named like a runtime
module (
io,async), which it would otherwise hide. - A dependency name that is not an identifier (
fast-json): it is the nameimportuses. - A dependency with no source or two, a repository not written
owner/name, or more than one ofversion,branch,tagandcommit. - A table or key
pep.tomldoes not have, or a value of the wrong type.
An import of a dependency that is not on disk says which folder is missing:
main.pep:1:8: error: dependency 'geometry' is not fetched: no /home/you/my_app/.pep/deps/github.com/someone/geometry
1 | import geometry;
| ^~~~~~~~
The language server reads pep.toml the same way, so imports of fetched
dependencies resolve in the editor with no -I.
Running a program: pep-run
pep-run builds a program and runs it straight away, the way
python script.py runs a script: there is no -o to choose and no
executable to clean up. pepper run runs it with the same arguments.
pepper run hello.pep # build hello.pep, then run it
pepper run args.pep a b # main sees a and b
pepper run -O2 -D FAST main.pep # build options go before the file
pepper run -- --port 8080 # the project pep.toml describes
pepper run --entry bench tool.pep # start in bench() instead of main()
pep-run [BUILD OPTIONS] [<entry.pep>] [ARGUMENTS]...
pep-run [BUILD OPTIONS] -- [ARGUMENTS]...
Arguments
- The arguments before the entry file are
pep-build’s options (-O2,-D NAME,-I DIR,-l LIB, …; see The compiler: pep-build), and they apply as they do forpepper build, as do a project’s[build]table and its[Link]libraries. - The entry file is the first argument that ends in
.pepand does not start with-. Everything after it goes to the program’smain, options included: inpepper run tool.pep --help,--helpis the program’s. --ends the build options too. With no entry file,pep-runbuilds the project whosepep.tomlis in the current folder or the nearest folder above it, aspepper builddoes, and the arguments after--go to it.- The options that choose what
pep-buildwrites are not taken:-o,--lib,--test,--targetand the--emit-...options are an error that exits with status 2.
| Option | What it does |
|---|---|
--entry <name> | Starts the program in the function name instead of main, as pep-build --entry does; it takes no parameters or one String[] (before the entry file only) |
-h, --help | Prints the usage (before the entry file only) |
-V, --version | Prints the version (before the entry file only) |
How it runs
pep-runrunspep-build(besidepep-run, else on thePATH) to build the program into a folder of its own:<Pepper home>/run/<key>/<entry stem>(mainfor a project), where the key is a hash of the current folder, the build options and the entry file, and the Pepper home isPEPPER_HOME, else~/.pepper(%LOCALAPPDATA%\pepperon Windows; see Toolchains: pep-install).- The build cache is
PEPPER_CACHE_DIRwhen it is set, else<Pepper home>/cache, which every run shares, so the standard library is compiled once for all your scripts. When nothing changed,pep-buildfinds the program up to date and the run starts at once; when something did, only what changed is compiled again. - A compile error is reported exactly as
pepper buildreports it, nothing runs, andpep-runexits withpep-build’s status. - Otherwise the program runs in the folder
pep-runwas started in, so a relative path such asdata.txtis found there, withpep-run’s standard input, output and error. On Linux and macOS it takespep-run’s place, so signals such as Ctrl-C reach it directly; on Windowspep-runwaits for it. Either waypep-runexits with the program’s exit status.
Two runs of one program at the same time are safe: the linker writes the executable to a temporary file and renames it into place, so neither ever starts a half-written program.
The run folders are not removed on their own; delete <Pepper home>/run
to reclaim the space.
The package manager: pep-package
pep-package fetches a project’s dependencies and keeps its pep.toml and
pep.lock. pepper package runs it with the same arguments:
pepper package add someone/geometry # the newest version, imported as 'geometry'
pepper package add other/json@2.x --name fast_json
pepper package remove geometry
pepper package update # every dependency, as far as the ranges allow
pepper package update --verify # one at a time, keeping the updates that build
pepper package fetch # what pep.lock lists
pep-package [-C <dir>] <command> [arguments]
It works on the project whose pep.toml is in the current folder or the
nearest folder above it (see Projects: pep.toml). Packages
are fetched with git when it is on the PATH; signing in to a private
repository is git’s concern (its credential helpers and SSH keys), so
whatever lets git clone reach a repository lets pep-package fetch it.
Without git, pep-package downloads the host’s archive of each commit
pep.lock pins over HTTPS instead (see Fetching without git),
so a project with a pep.lock builds on a machine with no git.
pepper build fetches on its own what pep.lock lists and .pep/deps
lacks, so a fresh clone of a project builds with pepper build alone.
Options and commands
| Command or option | What it does |
|---|---|
-C <dir>, --directory <dir> | Works on the project at or above <dir> instead of the current folder |
fetch | Fetches what pep.lock lists, and resolves what pep.toml changed since pep.lock was written |
add <owner/name>[@<range>] | Adds a dependency at its newest version, or at a range |
add ... --name <name>, -n <name> | The name source code imports it by |
remove <name> | Removes a dependency, by its name or its owner/name |
update [<name>...] | Moves dependencies to the newest versions their ranges allow; every one when none is named |
update --verify | Updates one dependency at a time and builds after each, keeping only the updates that build |
-h, --help | Prints the options; after a command, that command’s |
-V, --version | Prints the version |
The exit status is 0 on success, 1 when a command fails, and 2 for a mistake on the command line.
fetch
Fetches every package in pep.lock into the project’s .pep/deps at its
locked commit. When pep.toml names a dependency pep.lock does not list,
or asks for a revision the locked one does not satisfy, fetch resolves it
and updates pep.lock; everything else keeps its locked revision.
add <owner/name>[@<range>]
Adds a dependency to pep.toml’s [dependencies] and fetches it, keeping
the file’s comments and layout. An argument that starts with github:,
gitlab: or codeberg: (gitlab:group/pepper-json) names its host.
Without one, add looks the repository up on each host in the order
pep.toml’s [fetch] hosts lists (GitHub, GitLab, then Codeberg by
default; see Projects: pep.toml) and uses the first
that has it. When more than one does, it says so:
pep-package: other/numbers is on github and codeberg; adding github:other/numbers (prefix it with 'codeberg:' for another)
pepper package add someone/geometry # geometry = { github = "someone/geometry", version = "1.4.2" }
pepper package add someone/geometry@1.2.x # ... version = "1.2.x"
pepper package add codeberg:third/shapes # only Codeberg, without looking elsewhere
Without a range, the dependency gets the newest version tag (as a range
that accepts compatible later versions: 1.4.2 accepts 1.4.2 up to, not
including, 2.0.0); a repository with no version tags follows its default
branch. The name source code imports defaults to the repository’s last part,
with - and . turned into _ (other/json adds json); --name gives
another. A name already in pep.toml, or the name of a runtime module
(io, async), is an error that suggests --name.
remove <name>
Removes the dependency with that name (or that owner/name) from
pep.toml, and drops from pep.lock every package nothing needs any more.
update [<name>...]
Moves the named dependencies (every one, when none is named) to the newest version their ranges allow, and says which other package holds one back when one does:
pep-package: github:other/numbers stays at 1.3.1, not 2.0.0: geometry 1.1.0 wants 1
A dependency on a branch moves to the branch’s newest commit.
update --verify
Updates one dependency at a time and runs pep-build (beside
pep-package, else on the PATH) on the project after each. An update
that does not build is rolled back, and the command lists those it rolled
back.
pep.lock
pep.lock, beside pep.toml, records the exact commit of every package in
the dependency graph, its dependencies’ dependencies included, and the
sha256 checksum of its files. Commit it with pep.toml: it is what makes
a fresh clone build the same code.
version = 1
[[package]]
name = "geometry"
source = "github:someone/geometry"
version = "1.1.0"
commit = "0582024b9cd7a71044ee1aaa7099286f59e2df46"
sha256 = "7546a83a7f23d13454fd8b8f94281b6fd35f8dce49e9db9729c81208d35dacc8"
The checksum covers the files of the commit as the host’s archive of it
holds them, not the archive’s bytes, which a host may compress
differently from one download to the next. It is the SHA-256 of every
file, symlink and hard link, in path order, each written as its kind
(f, x for a file anyone may run, l, h), its path, its length and
its contents. With git, pep-package computes it from git archive of
the checkout, which holds the same files, so a lock written with git
verifies a later fetch without it. A lock from before checksums were kept
gains them on the next fetch.
A package whose files do not match is an error that names it and both checksums:
pep-package: error: cannot fetch github:other/numbers: the files of github:other/numbers 2c7a9e01d4f3 do not match pep.lock: sha256 9f2c..., expected 41b0...
Fetching without git
When git is not on the PATH, pep-package fetches each commit
pep.lock pins as the host’s archive of it, over HTTPS, checks its files
against the lock’s sha256 and unpacks it, without its top-level folder,
into .pep/deps:
| Host | Archive |
|---|---|
| GitHub | https://codeload.github.com/<owner>/<name>/tar.gz/<commit> |
| GitLab | https://gitlab.com/<owner>/<name>/-/archive/<commit>/<name>-<commit>.tar.gz |
| Codeberg | https://codeberg.org/<owner>/<name>/archive/<commit>.tar.gz |
An unpacked package has a .pep-commit file naming its commit in place of
.git. Choosing a revision still needs git, since it reads which tags
and branches a repository offers: add, update, and a fetch that must
resolve a dependency pep.lock does not pin (or a tag, which may have
moved) fail without it, saying so. Private repositories need git too, as
authentication is git’s concern.
Resolving versions
The whole project uses one version of each package: one checkout of
github:other/numbers serves every package that depends on it, whatever
name each imports it by.
- A version range picks the newest version tag (
v1.2.3or1.2.3) that every package’s range accepts. When none does, resolution fails and names each package’s requirement and the versions the repository offers. - A branch, tag or commit must be the same in every package that names it.
- The project’s own
pep.tomloverrides its dependencies: a package it names gets the revision it asks for, even outside the range a dependency declares, andpep-packagesays which requirement it overrode. - A locked revision stays while it still satisfies every requirement;
updateis what moves it.
Environment variables
| Variable | What it does |
|---|---|
PEPPER_PACKAGE_GITHUB_URL, PEPPER_PACKAGE_GITLAB_URL, PEPPER_PACKAGE_CODEBERG_URL | Fetches that host’s repositories from another address (a mirror, or a folder of repositories as a file:// URL): <address>/<owner>/<name>.git |
PEPPER_PACKAGE_GITHUB_ARCHIVE_URL, PEPPER_PACKAGE_GITLAB_ARCHIVE_URL, PEPPER_PACKAGE_CODEBERG_ARCHIVE_URL | Downloads that host’s archives, when git is missing, from another http:// or https:// address (a mirror): <address>/<owner>/<name>/<commit>.tar.gz |
PEPPER_RUNTIME | The runtime folder whose modules a dependency may not be named like, as for the compiler |
Building it
pep-package is written in Pepper (tools/pep-package) and reads and
edits pep.toml and pep.lock with the runtime’s
toml module, so building it needs no
network:
pepper build tools/pep-package/main.pep -o pep-package
Formatting with pep-fmt
pep-fmt rewrites Pepper source in one standard style, so that every Pepper
file reads the same and nobody has to argue about layout. It changes only
whitespace, line breaks, the order of imports, and the form of function
declarations; the program means exactly what it meant before.
pep-fmt is written in Pepper. It reads files with the Pepper compiler’s
own parser (tools/compiler/parse), so it accepts exactly the syntax the
compiler does.
Running it
Build it with the compiler, with the tools folder on the import path:
pepper build tools/pep-fmt/main.pep -o pep-fmt
Give it files or directories, like gofmt:
pep-fmt main.pep # print the formatted file to standard output
pep-fmt -w main.pep src/ # rewrite each file that is not formatted
pep-fmt --check src/ # list each file that is not formatted
pep-fmt < main.pep # no files: standard input to standard output
Options
pep-fmt [OPTIONS] [paths]...
| Option | What it does |
|---|---|
<paths> | Files to format, and directories to search for .pep files; none means standard input |
-w, --write | Rewrite each file that is not formatted, instead of printing it |
--check | Print the path of each file that is not formatted, and exit with 1 if there is one |
-h, --help | Print the options |
-V, --version | Print pep-fmt and its version |
-w and --check need at least one path. pepper fmt runs pep-fmt with
the same arguments.
A directory stands for every .pep file under it, in name order, skipping
directories whose names start with . (such as .git and the .pep build
cache). -w (or --write) rewrites only the files whose formatting changes. --check
changes nothing, prints the path of each file that -w would rewrite, and
exits with status 1 if there is one, which suits a CI step. The two can be
combined.
If a file does not parse, pep-fmt leaves it untouched, prints the first
syntax error to standard error, and exits with status 1 after the other
files. Standard input is named <stdin>:
main.pep:2:14: error: expected ')'
Errors the compiler finds after parsing, such as a number too large for its
type, do not stop pep-fmt. A few mistakes that parse, but whose text the
syntax tree does not keep (a second destructor, a second get or set in a
property, a second else arm, a property’s value written before its
accessors), are reported as cannot lay out this file and leave the file
untouched.
A file that cannot be read or written also exits with status 1, and an
unknown option, or -w or --check without a path, exits with status 2
after printing the usage (pep-fmt -h prints the full help). pep-fmt parses its options with the
cli module.
The style
Layout. Indentation is 4 spaces. A brace goes on the same line as the
declaration or statement that owns it, and else goes between the braces:
if count > 0 {
print($"{count}");
} else {
print("none");
}
A block stays on one line only when it holds a single break, continue
or return and was written on one line: if done { return; }.
Line width. Lines are at most 120 columns. A list that does not fit (arguments, parameters, an import list) moves to its own indented line, and if it still does not fit, puts one item on each line:
report_values(
first_argument, second_argument, third_argument, fourth_argument, fifth_argument, a_much_longer_argument
);
report_values(
first_argument,
second_argument,
an_argument_so_long_that_the_whole_list_no_longer_fits_on_one_line_even_after_moving_it_down_one
);
A long expression breaks before each operator, and a chain of three or more
method calls that does not fit breaks before each .:
if (first_condition_that_is_long(value) and second_condition_that_is_long(value)
or third_condition_that_is_long(value)) {
run();
}
total = first_value_that_is_long_enough
+ second_value_that_is_long_enough
+ third_value_that_is_long_and_longer_still;
var count = words
.iterate()
.filter((w) => w.length > 4)
.map((w) => w.length * 2)
.count_them_all();
Comments and string literals are never reflowed, so a line can stay longer than 120 columns when it has nowhere to break.
Attributes go one per line, each on its own line above the declaration:
[Target(OS(Linux))]
[Deprecated("use 'open' instead", "open")]
export open_file(path: String): int { return 0; }
A parameter’s attributes are the exception: they stay on the parameter’s line,
in front of it, as in construct([Sending] T value).
Declarations. Pepper functions and methods are written with the
parameter types after the names and the return type last,
add(a: int, b: int): int. The C-style form, int add(int a, int b), is
kept for extern functions, which mirror C prototypes; pep-fmt rewrites
it everywhere else, including operator methods (bool ==(Point other)
becomes ==(other: Point): bool, and bool !!() becomes !!(): bool),
indexers (T [](ulong index) becomes [](index: ulong): T) and constructor
parameters. A void return type is left out when the function has a body
(void run() and run(): void both become run()); a declaration without
a body, such as a trait method, keeps it.
Blank lines. At most one blank line in a row. Top-level structs, traits, extensions and functions with a body are separated by one blank line.
Comments stay where they are, attached to the code next to them. Trailing comments on consecutive lines are aligned, two spaces after the longest line:
var total = 0; # the running sum
var count = 0; # how many values
var average = 0.0; # total / count
Imports that are next to each other (no blank line or comment between
them) are sorted: modules first (import io;), then files of this module
(import "util";), each by name.
Kept as written: parentheses around
conditions (if x and if (x) are both fine), fields declared either way
(int x; or var x: int;), and the file’s line endings (\n or \r\n).
Turning it off for a section
Code between a # format: off comment and a # format: on comment is left
exactly as written, for layouts the style would undo, such as a table of
numbers:
# format: off
const IDENTITY = matrix(1, 0, 0,
0, 1, 0,
0, 0, 1);
# format: on
Each comment goes on its own line, between declarations or statements, and
the pair must be in the same block (or both at the top level). Without a
# format: on, the region runs to the end of the block or file.
Profiling with pep-profile
pep-profile runs a program under a sampling profiler and writes a
flame graph of where it
spends its time: an SVG that any browser opens, with one box per function,
as wide as the share of samples the function was on the stack for, above
the function that called it. Hover over a box to see its numbers; click it
to zoom in, and click “Reset zoom” to zoom back out.
pepper profile main.pep # build it, run it, write main.svg
pepper profile ./server -- --port 8080 # an executable, with its arguments
pepper profile --pid 4242 --duration 10 # a running process, for 10 seconds
pepper profile -i main.json -o main.svg # draw a recording made earlier
pep-profile records with a profiler that is already installed rather than
sampling the program itself:
| Profiler | Platforms | Install |
|---|---|---|
| samply | Linux, macOS, Windows | cargo install --locked samply |
| perf | Linux | your distribution’s linux-tools (or perf) package |
sample | macOS | comes with macOS |
It uses samply when it is installed, and otherwise perf on Linux or
sample on macOS; on Windows, samply is the one supported. When none is
installed it says what to install. --profiler picks one.
On Windows, samply records the system’s ETW events, which needs
administrator rights: run pep-profile from an Administrator terminal
(“Run as administrator”). Without them it stops at once and says so, rather
than waiting on a UAC prompt. Drawing a recording with --input needs no
rights.
pep-profile is written in Pepper (tools/pep-profile). Build it with the
compiler, with tools/ on the import path, and put it next to pepper so
that pepper profile finds it:
target/bin/pep-build tools/pep-profile/main.pep -o target/bin/pep-profile
Usage and options
pep-profile [options] <program.pep | executable> [-- arguments...]
pep-profile [options] --pid <PID>
pep-profile [options] --input <FILE>
| Option | What it does | Default |
|---|---|---|
<program> | The .pep file or executable to profile, then its arguments | |
-o <file>, --output <file> | The flame graph to write | the program’s name with .svg |
-F <hz>, --frequency <hz> | Samples per second | 999 |
-d <seconds>, --duration <seconds> | Stop recording after this long | when the program ends; 10 s with --pid |
-p <pid>, --pid <pid> | Record a running process instead of starting one | |
-i <file>, --input <file> | Draw a recording made earlier instead of recording | |
--profiler <name> | Record with samply, perf or sample, or the profiler at a path | the first installed |
--symbolizer <path> | The llvm-symbolizer that names frames, or none | found on the PATH |
-l, --lines | End each frame’s name with its file and line | off |
--idle | Keep the samples of threads that were waiting, not running (samply) | off |
--title <text> | The flame graph’s title | Flame graph: <name> |
--renderer <name> | What draws the flame graph: builtin, inferno or flamegraph.pl | builtin |
--folded <file> | Also write the folded stacks | |
--raw <file> | Also keep the profiler’s own recording | |
-I <dir>, --import-path <dir> | A folder pep-build searches for imports, when building a .pep file | |
--open | Open the flame graph when it is written | off |
-h, --help | Print the options | |
-V, --version | Print the version |
Give exactly one of a program, --pid and --input. The program’s own
arguments go after it; put -- before them when any starts with -, so
that pep-profile does not read them as its own options. pep-profile
exits with 0 once the flame graph is written, whatever the program’s own
exit status, with 1 when it cannot record or draw one, and with 2 for a
usage error.
Profiling a program
Given a .pep file, pep-profile first builds it with pep-build, found
next to pep-profile or on the PATH, as
pep-build main.pep -O -g --frame-pointers -o <scratch>/main
-O profiles the code that ships; -g gives the frames their names and
lines; and --frame-pointers keeps the frame pointer in every function, so
that the profiler walks each sampled stack quickly and completely (see
pep-build). Pass -I for each folder the
program imports from. An executable is run as it is: build it with the
same options to get the same detail.
The program runs with the terminal’s standard input and output. Recording
stops when it exits, or after --duration seconds. With perf, --duration
stops the program with timeout; with samply and sample the program
keeps running once recording stops, and sample then ends it.
--pid records a process that is already running, for --duration
seconds (10 by default). samply and perf need permission to do that: on
Linux, kernel.perf_event_paranoid must allow it (samply asks for 1 or
lower) or the profiler must run as root; on macOS, sample can attach to
your own processes.
samply also records each thread while it waits, as samples that used no
CPU. On Windows, a thread pool that Windows starts in every process sits
waiting the whole run, and alone would fill half the graph. So
pep-profile leaves out every sample of a samply recording in which its
thread used no CPU, and the graph shows where the program ran. --idle
keeps them, to see where threads block. It changes nothing with perf or
sample.
Names in the flame graph
Every frame is named the way the source names it:
- A function or method by its name:
work,Point.length,Box<int>.get. A generic specialization keeps its type arguments. - A closure by where it is written:
closure at main.pep:14. - A function the optimizer inlined still has a frame of its own, inside the function it was inlined into.
- With
--lines, a frame also gives the file and line it is at:work (main.pep:17). A function then gets one frame per line that was running, which shows where in a long function the time goes.
These names come from the program’s debug info, read by
llvm-symbolizer,
which LLVM installs: pep-profile looks for llvm-symbolizer and its
versioned names (llvm-symbolizer-20) on the PATH, then in
$PEPPER_LLVM_PREFIX/bin, $LLVM_SYS_<major>1_PREFIX/bin and the usual
LLVM install folders.
--symbolizer names one, and --symbolizer none skips it. Without one,
frames take the profiler’s symbol names, which pep-profile demangles:
Pepper’s linker name _PArray_3cint_3e.count shows as Array<int>.count,
and a closure as closure #0 in main. Inlined functions then have no
frames of their own, and --lines adds nothing.
Recordings
--raw <file> keeps the profiler’s recording: samply’s profile (with its
symbols beside it, in <file> with .syms.json in place of .json),
perf’s perf.data, or sample’s report. A samply profile opens in the
Firefox Profiler (samply load <file>),
which shows the samples over time; perf.data opens in perf report.
--input <file> draws a flame graph from such a recording instead of
making a new one, telling the format from what the file holds:
- a samply profile (JSON);
- a
perf.datafile (read withperf script), or the textperf script -F ip,sym,symoff,dsowrites; - the report
samplewrites; - folded stacks, as
--foldedwrites them.
Frames are named from the libraries the recording names, when they are still on this machine.
--folded <file> writes the folded stacks the flame graph is drawn from,
in the “collapsed” format other flame graph tools read: one line per
distinct stack, its frames from the outermost in, joined by ;, then a
space and its sample count.
_start;__libc_start_main_impl;main;work;Box<int>.get 533
Renderers
The flame graph is drawn by a renderer built into pep-profile, so it needs
nothing else installed. --renderer inferno draws it with
inferno’s inferno-flamegraph
(cargo install inferno), and --renderer flamegraph.pl with Brendan
Gregg’s flamegraph.pl, each
found on the PATH, for their extra features such as search.
Testing with pep-test
pep-test builds the [Test] functions of Pepper files and runs them,
each in a child process of its own, reporting which pass. Writing tests is
covered in Unit tests.
pepper test # every test under the current folder
pepper test src/words_test.pep # the tests of one file
pepper test -f words src # only tests whose name contains "words"
pepper test --list src # list them without running them
pepper test -I . geometry/tests # tests that import the module geometry
running 3 tests
test counts_the_words ... ok (4 ms)
test an_empty_string_has_no_words ... ok (3 ms)
test reading_past_the_last_word_panics ... ok (5 ms)
3 passed, 0 failed
pep-test is written in Pepper (tools/pep-test). Build it with the
compiler and put it next to pepper and pep-build, so that pepper test
finds it and it finds the compiler:
target/bin/pep-build tools/pep-test/main.pep -o target/bin/pep-test
Usage and options
pep-test [options] [path]...
| Option | What it does | Default |
|---|---|---|
<path> | A .pep file to test, or a folder to search for files holding [Test] functions; repeatable | the current folder |
-f <text>, --filter <text> | Run only the tests whose name contains the text; repeated, any of them | every test |
-l, --list | List the tests instead of running them | |
-j <count>, --jobs <count> | How many tests run at once | one per logical CPU |
-t <seconds>, --timeout <seconds> | Fail a test still running after this long; 0 for no limit | 60 |
-I <dir>, --import-path <dir> | A folder pep-build searches for imports; repeatable | |
-L <dir>, --library-path <dir> | A folder the linker searches for libraries, for pep-build; repeatable | |
-D <option>, --define <option> | A build option for pep-build, NAME or NAME=VALUE; repeatable | |
-h, --help | Prints the usage and these options | |
-V, --version | Prints pep-test and its version |
The exit status is 0 when every test program builds and every test passes, 1 when a test fails or a program does not build, and 2 for a usage error.
What it does
- Finds the files. A file named on the command line is tested as it
is. In a folder, and the folders below it (except those whose names
start with
.), it takes each.pepfile with a line that starts with[Test]. - Builds one test program per folder, of that folder’s test files,
with
pep-build --test(found next topep-test, else on thePATH), in parallel. Each file keeps the scope it has in any program, so test files in a module’s folder see each other’s names, and a helper two of them declare must beprivate(see Unit tests). A program that does not build is reported with the compiler’s errors, and the other folders’ tests still run. - Lists each program’s tests with
--listand keeps those that match--filter. With--listit prints them and stops: the test’s name, orfile::namewhen the tests come from more than one file. - Runs each test as
program --run FILE::NAME --timeout SECONDS, each in its own process, up to--jobsat once, and printstest NAME ... ok (TIME)or... FAILED (TIME)as each ends (in order with--jobs 1). TIME is how long the test’s process ran: whole milliseconds (12 ms), or seconds with two decimals from one second up (1.25 s). A test passes when its process exits with 0; a[ShouldPanic]test passes when its process panics with a message that contains the attribute’s text. - Reports the failures, each with what the test printed (the panic’s
message and location) and why it failed, then a summary:
2 passed, 1 failed, with the number of tests--filterleft out and of test programs that did not build when there are any.
The test programs are built in a temporary folder that is removed at the
end. A test runs in the current folder, with PEPPER_BACKTRACE=0 unless
PEPPER_BACKTRACE is set, so a panic prints its message without a
backtrace. Each test gets a temporary folder of its own (TMPDIR, and
TMP and TEMP for Windows, name it), removed when the test ends, so
what a test that panics or times out leaves there, such as a
TempFolder whose destructor
never ran, is removed too.
The test program
pep-build --test builds a folder’s test program (see pep-build),
which pep-test runs with one of two requests:
--listprints a line per test, its fields separated by tabs: the name,file:line,returnsorpanics(a[ShouldPanic]test), and the text the panic must contain.--run NAME [--timeout SECONDS]runs one test (NAME, orFILE::NAMEwith the file as--listgives it) and exits with 0 when it returns having released everything it allocated; a test that leaves an allocation behind (or releases one too many) exits with 1 and says so on standard error. A panic aborts the program; a test that runs past the timeout ends it with exit status 124. However the program ends (the test returns, panics, crashes or times out), every process the test started ends with it (a job object on Windows, a process group elsewhere): one left running would hold the pipes to its output open, andpep-testwould wait on it.
On Windows, a test program turns off Windows Error Reporting for its
crashes and abort(), so a test that panics or crashes ends at once,
without a dialog.
The language server: pep-lsp
pep-lsp is Pepper’s language server. An editor starts it and talks to it
over standard input and output with the
Language Server Protocol:
it reports compiler errors and warnings as you type, and provides hover,
completion, go to definition, document outlines and inlay hints. It checks
code with the compiler’s own front end, and finds the standard library the
way the compiler does (see
Where the compiler finds the standard library).
Warnings in the files a document imports show on those files too, such as a use of a deprecated function in a library the document calls. A file open in the editor shows its own analysis instead.
The VS Code extension bundles
it. When pepper install manages your Pepper home, the
extension instead runs the pep-lsp of the toolchain the first workspace
folder uses, as pepper install which reports it there, so the editor
checks code with the same compiler and standard library as pepper build.
If that fails (say the project pins a toolchain that is not installed), a
warning says why and the bundled server runs. Run Pepper: Restart
Language Server after switching toolchains.
The extension also offers to install what is missing, running it as a task whose terminal shows the progress and any error, then offers to reload the window to pick the toolchain up:
- with no Pepper home, Install Pepper runs the install script;
- when the toolchain the workspace uses is not installed, Install runs
the
pepper install add <version>(orpepper install update) thatpepper install whichsuggests; - when the default toolchain is older than the extension, Update runs
pepper install update. A toolchain a project pins, orPEPPER_TOOLCHAINnames, is left as it is.
Don’t Ask Again stops an offer for good. For another editor, run the
toolchain’s pep-lsp (or pepper lsp) for .pep files.
The server is written in Pepper, in tools/pep-lsp, on the
lsp package, a framework
for writing a language server for any language in Pepper. To build it from
a checkout of the repository, with this checkout’s standard library:
PEPPER_RUNTIME=$PWD/runtime pep-build -O tools/pep-lsp/main.pep -o target/pep-lsp/pep-lsp
When the server crashes
pep-lsp runs the server in a child process (pep-lsp --serve) and
passes messages between the editor and it. If the server crashes (a bug in
the compiler or the server ends it with a panic), pep-lsp answers the
requests it left unanswered with an empty result, logs the server ended ...; restarting it to the editor’s output, and starts a new one, which
gets the open documents again. If the new one crashes too before you
change anything, the document you changed last is left out until you
change it again, so one text that crashes the server cannot keep it down;
after five crashes in a row pep-lsp gives up and exits.
Checking as you type
The server checks a file as the root of its own program, the way
pepper build checks one: one compilation unit at a time, starting with
the standard library and working up through each file’s imports. A
compilation unit is one file, or the files that import each other in a
cycle. It saves what it learned about each compilation unit in the build
cache, in the .pep/cache folder of the
file’s project (the folder holding its pep.toml, else the file’s own
folder), or in $PEPPER_CACHE_DIR when that is set. When you edit a file,
only the file you are editing is checked again. Every other compilation unit loads
from the cache, unless its files changed on disk or a declaration it
depends on changed. The cache outlives the server, so a restarted editor
answers quickly once the files have been checked once. Delete the folder
to start over.
The first time, when the cache has nothing yet (it lacks even the standard
library’s compilation unit), the server checks the whole program at once,
which is quicker than one compilation unit after another, and meanwhile a
copy of itself (pep-lsp --fill-cache) fills the cache: it checks the
compilation units the file depends on in parallel, in more copies
(pep-lsp --compilation-unit-worker), one per processor at most, as
pepper build does. Until it is done, an edit is checked again on the
record of the whole program the first check left: only the bodies and
declarations the edit changed or added are checked. An edit that does more,
or that has an error or a warning, waits for the copy, then loads the
compilation units it checked. When the
cache has some compilation units but not others, the server checks the
missing ones that way itself.
Command-line options
pep-lsp [--stdio]
| Option | What it does |
|---|---|
--stdio | Talk over standard input and output: the default, accepted for editors that pass it |
--serve | Internal: run the server in this process, without the supervisor that restarts it. The supervisor starts it so |
--compilation-unit-worker <dir> | Internal: check compilation units for the server that started it, which passes them through <dir>. The server starts these itself. --layer-worker is its deprecated spelling |
--fill-cache <dir> | Internal: check the compilation units the build cache lacks, in --compilation-unit-worker copies, for the server that started it, which passes the program through <dir>. The server starts it itself when the cache is empty |
-h, --help | Print the options and exit |
-V, --version | Print pep-lsp and its version, and exit |
Any other argument is an error, and the server exits with status 2 without starting.
Initialization options
The server’s settings arrive in the initializationOptions of the LSP
initialize request, read once when it starts:
{
"inlayHints": { "implicitConversions": true, "parameterNames": true },
"buildOptions": ["USE_ZLIB", "LOG_LEVEL=2"]
}
| Option | Type | Default | What it does |
|---|---|---|---|
inlayHints.implicitConversions | boolean | true | Show the constructor call an [Implicit] conversion inserts, such as Path( … ) around a String passed where a Path is expected |
inlayHints.parameterNames | boolean | true | Show the parameter name before a literal argument passed by position (resize(width: 800, height: 600)) |
buildOptions | array of strings | [] | The build options to analyze with, as the compiler’s -D takes them: NAME or NAME=VALUE. One that does not parse is left out. A document that declares a [Test] function is also analyzed with TEST, and testing loaded, as pep-test builds it |
VS Code settings
The extension passes its settings to the server as the initialization options above, and restarts the server when one changes:
| Setting | Default | What it does |
|---|---|---|
pepper.lspPath | empty | The pep-lsp to run; empty uses the project’s toolchain’s, else the one bundled with the extension. The server restarts whenever that file is rebuilt |
pepper.inlayHints.implicitConversions | true | inlayHints.implicitConversions |
pepper.inlayHints.parameterNames | true | inlayHints.parameterNames |
pepper.buildOptions | [] | buildOptions |
Inlay hints also need VS Code’s own editor.inlayHints.enabled. The command
Pepper: Restart Language Server (pepper.restartServer) restarts the
server on demand.
The documentation builder: pep-doc
pep-doc builds this book. It generates the
standard library reference from the runtime’s
documentation comments, checks the book, and
renders it with mdBook, which must be
on the PATH (a binary from
its releases).
It is a Pepper program (tools/pep-doc), and it works on the Pepper
checkout that holds the folder it is run in. scripts/pepper-tests.sh
builds it beside pep-build, with the other Pepper tools, and pepper doc
runs it. To build it alone, from the repository root:
pepper build -O tools/pep-doc/main.pep -o target/bin/pep-doc
target/bin/pep-doc reference
pep-doc [<command>] [options]
Commands
pep-doc reference
Regenerates docs/src/reference/ and the reference’s entries in
docs/src/SUMMARY.md from the doc comments under runtime/, and prints each
file it changed. Run it after changing a runtime doc comment, and commit the
result.
pep-doc check
Changes nothing, and fails (exit status 1) when:
- the committed reference does not match the runtime, naming the stale pages;
- a chapter has Markdown that renders broken without an mdBook error, such as an include indented inside a list item;
- a public declaration in
runtime/(ortools/syntax) has no doc comment, or one without a description or a tag the documentation standard requires, as a clang-style error at the declaration; - a tool’s option, command, option value, trace channel, environment variable or setting is missing from the tool’s page in this book, naming the tool and the page;
- a Pepper tool’s
version()(intools/<tool>/main.pep, or pep-lsp’s intools/pep-lsp/arguments.pep) is not the version in the repository’sVERSIONfile, whichscripts/set-version.shsets them all to.
The tools’ options come from their --help: pep-build‘s and the Pepper
tools’ (pepper, pep-fmt, pep-profile, pep-package, pep-install,
pep-run, pep-test), which scripts/pepper-tests.sh builds in
target/pepper-tests/bin beside a link to its pep-build, so run it
first.
--tools <DIR>: the folder holding the built tools (default:target/pepper-tests/binunder$PEPPER_TARGET_DIRortarget/).
CI runs it, after the tests.
pep-doc build
Regenerates the reference, renders the book into <site>/<version>/, and
refreshes the site’s versions.json and index.html. Other versions in the
site are left as they are, so every published version stays available. This
is the command when none is given.
--version <VERSION>: the version to build,X.Y.Zordev(the default). Anything else is an error.--site <DIR>: the site folder (default:target/docs-site).
pep-doc build --version 0.3.0 --site site # renders into site/0.3.0/
pep-doc serve
Regenerates the reference and serves the book locally with mdbook serve,
opening it in the browser and rebuilding as chapters change.
pep-doc help
Prints the commands and options, as -h and --help do.
Options
| Option | What it does |
|---|---|
--version <VERSION> | With build: the version to build |
--site <DIR> | With build: the site folder |
-h, --help | Print the commands and options |
-V, --version | As the only argument: print pep-doc and its version |
An unknown command is an error. Each command exits with 0 on success and 1 on failure, with the reason on standard error.
See Building this documentation for how the book is organized and published.
Debugging
Build with -g to debug a Pepper program in gdb, lldb, Visual Studio or
WinDbg. The compiler then describes the program to the debugger, which can:
- stop at a
file:linebreakpoint; - step through the source one statement at a time;
- show a backtrace with the source line of every Pepper frame;
- print parameters, locals, module-scope variables and struct fields.
pepper build -g main.pep -o main # Linux: DWARF in the executable
pepper build -g main.pep -o main # macOS: DWARF, and main.dSYM beside it
pepper build -g main.pep -o main.exe # Windows: CodeView, and main.pdb beside it
On macOS the linker leaves the DWARF in the object files, so -g also runs
dsymutil (from Xcode’s command line tools) to gather it into
main.dSYM. lldb finds the bundle beside the program; so do the program’s
own backtraces, which read their source lines from
it. Without dsymutil the program still builds, and its backtraces name
functions but not lines.
-g and -O work together. Optimized code still steps and shows a
backtrace, and a call the optimizer inlined still appears as its own frame.
A variable the optimizer removed shows as optimized out.
A session
This program builds an array of points and adds up the first one:
# Build with `-g` to debug this program: break on line 18 and print `first`.
struct Point {
var x: int = 0;
var y: int = 0;
}
total(p: Point): int {
return p.x + p.y;
}
main(): int {
var points = Array<Point>();
var p = Point();
p.x = 1;
p.y = 2;
points.append(p);
var first = points[0];
return total(first) - 3;
}
Stop on line 18, after first is assigned, and look around. In gdb:
$ pepper build -g debugging.pep -o debugging
$ gdb ./debugging
(gdb) break debugging.pep:18
(gdb) run
Breakpoint 1, main () at debugging.pep:18
18 return total(first) - 3;
(gdb) info locals
points = 0x5555555592f0
p = 0x5555555592c0
first = 0x5555555592c0
(gdb) print *first
$1 = {__refcount = 3, __weakcount = 1, x = 1, y = 2}
(gdb) step
total (p=0x5555555592c0) at debugging.pep:8
8 return p.x + p.y;
(gdb) backtrace
#0 total (p=0x5555555592c0) at debugging.pep:8
#1 main () at debugging.pep:18
lldb takes the same steps with b debugging.pep:18, r, frame variable
and bt. In Visual Studio, open the executable as a project (File > Open >
Project/Solution), set a breakpoint in the source file and start debugging.
The .pdb beside the executable is found automatically.
What the debugger sees
-
Functions under the names you wrote:
total,Point.length,Array<int>.append. Methods the compiler writes for a struct, such as__acquireand__release, are marked artificial and placed on the struct’s line. A closure is named for where it is written:closure at main.pep:14. -
Structs. A variable of a struct type holds a reference, so the debugger shows an address. Dereference it (
print *first) to see the fields. The fields include the__refcountand__weakcounthead, which helps when chasing an ownership bug. -
Numbers under their Pepper names (
int,ulong,u8). -
Enums. A plain enum shows its case by name. An enum with payloads shows as a struct holding its tag,
__tag, whose value names the case (Rect), and the payload fields of every case, each namedCase__field(Rect__width), which a debugger expression can name:print shape.Rect__width. With Pepper’s formatters loaded (see below) it shows only its case and that case’s values instead. A value enum ([Value], such asOptionalandResult) is that struct itself, held inline, with no reference counts. -
Tuples as a struct with members
0,1, and so on. -
Trait values as the object they refer to and its vtable.
-
Generics. Each instantiation is its own type, such as
Array<long>. -
Module-scope variables under their names.
-
String,ArrayandOptionalread naturally once the debugger loads Pepper’s formatters (see below): aStringas its text, anArrayas its count and elements, anOptionalas its case,EmptyorValue, and its value. AStringand anArrayalso show their reference counts as[ref count](strong references) and[weak count]. -
Other enums with payloads read the same way with the formatters: as their case and that case’s values, such as
Rectwithwidthandheight.
Stepping through a static block
A static block unrolls its compile-time
loops into one pass per element, and the debugger steps through those
passes as it would a loop that runs: it stops on the for (or while)
line at the start of each pass, then on each line of the body. A
compile-time if stops once on its line, then in the branch it kept. The
compile-time variables (the loop’s field, a var of the block) are
locals of each pass, so the debugger shows which pass it is in:
# Build with `-g` and step through `total`: each pass of the `static`
# block's loop stops on the `for` line, then on its body, with `field`
# holding the pass's member.
struct Point {
var x: int = 1;
var y: int = 2;
var z: int = 3;
}
total(p: Point): int {
var sum = 0;
static (sum, p) {
for (var field in reflect.properties<Point>()) {
sum = sum + field.get(p);
}
}
return sum;
}
main(): int {
return total(Point()) - 6;
}
$ pepper build -g debugging-static.pep -o debugging-static
$ gdb ./debugging-static
(gdb) break total
(gdb) run
(gdb) next
11 var sum = 0;
(gdb) next
13 for (var field in reflect.properties<Point>()) {
(gdb) next
14 sum = sum + field.get(p);
(gdb) print field
$1 = {name = 0x55555559073f "x", type_name = 0x555555590a57 "int", index = 0, is_last = false, is_const = false, is_property = false}
(gdb) next
13 for (var field in reflect.properties<Point>()) {
(gdb) next
14 sum = sum + field.get(p);
(gdb) print field.name
$2 = 0x5555555909ba "y"
To step through the code each pass emits instead, with the compile-time
values filled in, build with --debug-static=expanded. Beside the program
it writes debugging-static.static.pep, the source with the block
expanded as --emit-static
writes it, and the debugger shows that file for the block’s code:
$ pepper build -g --debug-static=expanded debugging-static.pep -o debugging-static
$ gdb ./debugging-static
(gdb) break debugging-static.static.pep:18
(gdb) run
Breakpoint 1, total (p=0x55555559c6b0) at debugging-static.static.pep:18
18 sum = sum + p.y;
(gdb) print field.name
$1 = 0x5555555909ba "y"
The rest of the function, and the program, stays in its own files.
A breakpoint on a line of the body stops in every pass. A member or case
shows as a structure of what reflect reads from it (name, index,
is_last, and so on); a list does not show. The stop on the header is a
nop the compiler adds only to unoptimized -g builds; optimized code
steps through the body’s lines without it.
Formatters for the runtime’s types
The runtime ships formatters for each debugger in runtime/debuggers/.
The sessions below stop in a program holding var name = "pepper";, an
Array<int> values of 3 and 4, and var found = Optional<int>.Value(7);.
-
gdb. On Linux a
-gprogram carriesgdb_printers.pyin its.debug_gdb_scriptssection. gdb loads it from directories on its auto-load safe path, so add the program’s directory to it, on the command line or in~/.gdbinit:gdb -iex "add-auto-load-safe-path ." ./debugging(gdb) print name $1 = "pepper" = {[ref count] = 1, [weak count] = 1} (gdb) print values $2 = Array<int> of count 2 = {[ref count] = 1, [weak count] = 1, [0] = 3, [1] = 4} (gdb) print found $3 = Value = {value = 7} -
lldb. Import
lldb_formatters.py, once per session or in~/.lldbinit:(lldb) command script import runtime/debuggers/lldb_formatters.py (lldb) frame variable name *values found (String *) name = 0x00005555555592c0 "pepper" (Array<int>) *values = count=2 { [ref count] = 1 [weak count] = 1 [0] = 3 [1] = 4 } (Optional<int>) found = Value { value = 7 } -
Visual Studio and WinDbg. A
-gprogram’s.pdbcarriespepper.natvis, and an entry for each of the program’s enums with payloads, so nothing needs loading.
Building with -g
Debug info changes the code each file compiles to, so -g builds keep
their own objects in the build cache, apart from builds without it. With
-g, editing a file recompiles the files that import it, even when the edit
only moves a declaration: their code records the line numbers of the
generics they instantiate from it.
Backtraces without a debugger
A program can print its own stack: a panic does in a build without -O,
and the debug module’s capture() and print_backtrace() do on request.
debug.output, debug.is_debugger_attached and debug.breakpoint talk to
an attached debugger. See Backtraces and the debugger.
Standard library reference
The standard library is the Pepper runtime: the files under runtime/ that
every program is compiled with. The pages listed first (from Built-in
functions to URI) are the prelude, available without an import. Each module
after them (io, async, json, …) is brought in with an import, such
as import io or import [File, Path] from io; its other pages are listed
under it.
These pages are generated from the doc comments in the runtime’s source (see Documentation comments), so they always match the version of Pepper they were built with. Use the version picker at the top of the page to read the reference for another release.
The prelude
The prelude is the part of the standard library every program gets without an import: the files at the top of runtime/. Each module after it (io, async, json, …) is brought in with an import.
- Built-in functions:
assertandpanic, which every program can call without an import. - Built-in attributes: The built-in attributes, declared as
[Attribute]structs so astaticblock can read them like its own:field.has_attribute<Deprecated>()tests for one andfunction.attribute<Deprecated>().messagereads its value (see the reflection chapter). - The reflect module (compile time):
reflectexists only inside astaticblock, which runs while compiling. - Compiler built-ins: The types and functions the compiler provides itself: raw pointers, weak references, atomics, checked arithmetic and the other
__intrinsics the runtime is built on. - Arguments and exit (deprecated): The running program’s arguments and exit, kept for the deprecated prelude
arguments()andexit(); theprocessmodule has them now. - String: A mutable, reference-counted string of UTF-8 text.
- Numbers: Reading numbers from text, and arithmetic that says what to do on overflow.
- Comparable: Ordering:
Comparable, the trait of types whose values sort in one order, which every number type,charandStringdeclare. - Ranges: Ranges of numbers:
a..banda..>b. - Array: A growable, contiguous, zero-indexed sequence of
Tvalues. - HashMap and HashSet: Hashing:
hash<T>(value), the 64-bit hashHashMapandHashSetuse for their keys, and the hash maps and sets themselves. - Map and Set: Sorted collections:
Map<K, V>andSet<K>, kept in key order on a red-black tree, andcompare<T>, the ordering they sort keys by. - OrderedMap:
OrderedMap<K, V>: a hash map that keeps its keys in the order they were first inserted. - BitSet:
BitSet: a set of small non-negative integers, stored one bit each. - Deque:
Deque: a double-ended queue over a growable ring buffer. - PriorityQueue:
PriorityQueue: a binary heap that hands out its greatest element first. - Iterators: Something a
forloop can step through, one element at a time. - Scoped: The scope protocol behind
with (var x = scope) { ... } else { ... }. - Weak: Weak references:
Weak<T>points at an object without keeping it alive. - Handle:
Handle<T>: owns a resource that Pepper’s reference counting does not know about, such as a C library’s object, and releases it when the last reference to the handle goes away, like C#’sSafeHandle. - Result:
Result<T, E>: the value of an operation that can fail, eitherOkholding its value orErrholding its error, like Rust’sResult. - Optional: A value of type
Tthat may be absent: either Value, holding aT, or Empty, holding nothing, like Rust’sOption<T>and Swift’sOptional. - Copying: Deep copies:
copy(value)makes a copy that shares no object that is not thread-safe with the original, so it can be handed to another thread;Copyable<T>lets a type say how it is copied. - URI: URI references (RFC 3986): parse any URI, such as
https://example.com/a?b#c,mailto:someone@example.comor/items?page=2, into its parts. - Test checks: Checks for tests:
expect_equal,expect_not_equal,expect_trueandexpect_false, which every program can call without an import.
Built-in functions
assert and panic, which every program can call without an import.
Source: runtime/internals.pep
assert
assert(condition: bool, message: String)
Panics with message when condition is false; otherwise does nothing.
The check always runs; there is no release mode that removes it. A
string literal message picks the __ptr<char> overload instead, which
builds no String. Any other message is evaluated before the call.
Parameters
condition— the condition that must holdmessage— the text to print on failure; a nullStringprints a generic message
Panics
- if
conditionis false: prints the location andmessage, and aborts (seepanic)
Example
assert(count > 0, "count must be positive");
assert
assert(condition: bool, message: __ptr<char>)
Checks that condition holds; if not, panics with message, a
NUL-terminated C string. A string literal message picks this overload,
so an assertion that holds builds no String.
Parameters
condition— the condition that must holdmessage— the text to print on failure, or__nullfor a generic message
Panics
- if
conditionis false: prints the location andmessage, and aborts (seepanic)
Example
assert(count > 0, "count must be positive");
panic
panic(message: String)
Prints message and aborts the program. It never returns.
Flushes every C output stream, so output printed so far is not lost,
then writes location: panic: message and a newline to standard error
(“Assertion failed with no additional information” when message is
null), followed in a build without -O by a backtrace of the calls
that led there (PEPPER_BACKTRACE overrides the build; see the debug
module’s print_backtrace), and calls C abort(). No destructors or
releases run, and the process ends abnormally (SIGABRT on POSIX).
The location is the call’s file:line:col, which the compiler passes
in a hidden parameter, the way a C++ std::source_location default
argument works. A function that panics on its caller’s behalf, as the
runtime’s indexers do, declares that parameter itself and passes it on,
so the panic names its caller’s line:
check_positive(value: int, location: __ptr<char> = __caller_location()) {
if (value <= 0) {
panic("expected a positive value", location);
}
}
Parameters
message— the text to print
Panics
- always
Example
panic("unreachable");
panic
panic(message: __ptr<char>)
Reports message, a NUL-terminated C string (a generic message when it
is __null), as a panic and aborts, as panic(String) does. A string
literal message picks this overload, so it allocates nothing.
Parameters
message— the text to print, or__null
Panics
- always
Example
panic("unreachable");
Built-in attributes
The built-in attributes, declared as [Attribute] structs so a static
block can read them like its own: field.has_attribute<Deprecated>()
tests for one and function.attribute<Deprecated>().message reads its
value (see the reflection chapter). The compiler acts on each attribute
itself; these declarations only describe the values it records.
Source: runtime/attributes.pep
Attribute
[Attribute] struct Attribute
Marks a struct as an attribute, which can then be written above a
declaration and read back in a static block.
Deprecated
[Attribute] struct Deprecated
Marks a public declaration as deprecated: each use outside a deprecated
declaration warns with message.
Fields
message
const message: String
What the warning says, such as “use ‘open’ instead”.
replacement
const replacement: String = ""
The name that replaces the declaration, offered as a fix; empty when there is none.
Alias
[Attribute] struct Alias
Gives a parameter a second permanent name that a call may use:
[Alias("size", "count")] lets a call name count as size.
Fields
alias
const alias: String
The second name.
parameter
const parameter: String
The parameter it names.
Rename
[Attribute] struct Rename
Keeps a parameter’s former name working behind a deprecation warning:
[Rename("n", "count")].
Fields
old_name
const old_name: String
The former name.
new_name
const new_name: String
The parameter’s name now.
Link
[Attribute] struct Link
Names the C library an extern links against, its C symbol, or both:
[Link("m")], [Link(name = "sym")], [Link("ssl", path = "vendor")].
Fields
lib
const lib: String = ""
The library; empty when only the symbol is given.
path
const path: String = ""
A folder the linker searches for libraries, relative to the file that declares the extern unless absolute; empty for none.
name
const name: String = ""
The C symbol, when it differs from the declared name; empty otherwise.
Target
[Attribute] struct Target
Keeps a declaration only on the platforms its condition matches:
[Target(OS(Linux))], [Target(!OS(Windows))].
Fields
condition
const condition: String
The condition as written, such as !OS(Windows).
BuildOption
[Attribute] struct BuildOption
Keeps a declaration only when the build defines a build option with
-D: [BuildOption("USE_ZLIB")] is [Target(BuildOption(USE_ZLIB))].
Fields
name
const name: String
The build option’s name, such as USE_ZLIB.
Likely
[Attribute] struct Likely
Marks the if or match arm below it as the branch usually taken,
so the optimizer lays it out as the straight-line path.
Unlikely
[Attribute] struct Unlikely
Marks the if or match arm below it as a branch rarely taken, such
as error handling, so the optimizer moves it out of the fast path.
NotCopyable
[Attribute] struct NotCopyable
Forbids copying a struct with copy(value), giving the reason in the
error.
Fields
reason
const reason: String
Why the struct cannot be copied.
Value
[Attribute] struct Value
Makes a struct a value: stored inline and copied on assignment, with no reference count.
Equatable
[Attribute] struct Equatable
Makes a struct, or an enum whose cases carry values, compare and hash
by its fields, like a tuple of them, instead of by identity: the
compiler derives its == and its hash, so equal values find each other
as HashMap and HashSet keys.
On a field of such a struct, it says whether the field takes part:
[Equatable(exclude = true)] or [Equatable(include = false)] leaves it
out of both == and the hash. A field with neither takes part. On the
struct itself it takes no arguments.
Fields
include
bool include = true
Whether the field takes part in == and the hash; false leaves it
out.
exclude
bool exclude = false
Whether the field is left out of == and the hash.
Shared
[Attribute] struct Shared
Makes a struct safe to share between threads: its counts step atomically
and its fields must be const and thread-safe.
ThreadLocal
[Attribute] struct ThreadLocal
Gives a module-scope var a separate value on every thread, like C++
thread_local: each thread starts from the initializer, which must be a
constant, and reads and writes only its own copy, with no locks or atomics.
The variable’s type must be thread-safe (a number, a raw pointer, a
[Shared] struct, a shared function type); for any other type, use
async.ThreadLocal<T>. A struct value is released when a thread started
with async.Thread.spawn exits; the main thread’s, like every other
module-scope variable, is never released.
Example
[ThreadLocal]
var calls: int = 0;
count_call() {
calls += 1;
}
Isolated
[Attribute] struct Isolated
Makes a struct a guard that can only be the scope of a with block and
never escapes it.
Implicit
[Attribute] struct Implicit
Lets a one-parameter constructor convert implicitly wherever a value of its parameter’s type flows into the struct’s type.
Move
[Attribute] struct Move
Marks a parameter that takes its argument’s reference: the caller’s variable may not be used again.
MoveSource
[Attribute] struct MoveSource
Marks the parameter of a function that moves a value out of its source,
as move does.
Locked
[Attribute] struct Locked
Marks a function-typed parameter that is called while a lock is held, so it may use the locked value.
Unsafe
[Attribute] struct Unsafe
Removes runtime checks from one function’s body, as
--no-runtime-checks removes them from a whole build:
[Unsafe("overflow")], [Unsafe("overflow,bounds")] or
[Unsafe("all")]. It applies at every optimization level. Calls the
function makes keep their own checks, except the index check that an
Array or String method makes for its caller, which bounds removes
too. Removing bounds or null gives up memory safety in that function.
Fields
checks
const checks: String
The checks to remove, comma-separated: overflow, divide, shift,
bounds, null, or all.
Cold
[Attribute] struct Cold
Marks a function rarely called, such as an allocator’s slow path or error handling, so the optimizer never inlines it and its callers’ fast paths stay small enough to inline themselves.
Test
[Attribute] struct Test
Marks a function as a unit test: pep-test (pepper test) builds it
into a test program and runs it in a child process of its own. The
function takes no parameters and returns nothing; it passes when it
returns and fails when it panics (see expect_equal). A build without
--test leaves [Test] functions out.
ShouldPanic
[Attribute] struct ShouldPanic
Marks a [Test] function as one that passes only when it panics:
[ShouldPanic] takes any panic, and [ShouldPanic("index out of range")] only a panic whose message contains that text.
Fields
message
const message: String = ""
Text the panic’s message must contain; empty for any panic.
Export
[Attribute] struct Export
Makes a free function callable from C, in a library built with
--lib static or --lib shared: [Export], or
[Export(name = "c_name")] for a C symbol other than its name.
Fields
name
const name: String = ""
The C symbol, when it differs from the function’s name; empty otherwise.
__BuiltIn
[Attribute] struct __BuiltIn
Declares a compiler built-in in the runtime, for the reference and the
editor: the compiler ignores the declaration, whose functions end in ;.
[__BuiltIn("reflect.name")] names the built-in when it is not the
declaration’s own name. Only the runtime may use it.
Fields
name
const name: String = ""
The built-in’s name, when it differs from the declaration’s; empty otherwise.
The reflect module (compile time)
reflect exists only inside a static block,
which runs while compiling. It needs no import. Everything here is
evaluated by the compiler: the values are compile-time numbers, bools,
strings, lists, and the members, cases, functions and attributes below.
The Reflection and attributes chapter
explains how a block mixes them with the code it emits.
The compiler provides reflect itself; this file only declares it, with
[__BuiltIn], for this reference and the editor.
Source: runtime/reflect.pep
reflect.name
reflect.name<T>(): String
The name of type T as it is written in source, with its type arguments
("Box<int>", "Array<String>").
Type parameters
T— the type to name
Returns
- the type’s name
reflect.properties
reflect.properties<T>(): List<Member>
The public fields and properties of struct
T, in declaration order, as members.
Type parameters
T— a struct
Returns
- one member per public field or property
reflect.fields
reflect.fields<T>(): List<Member>
Every field of struct T, private ones and a property’s storage included,
in declaration order, as members. It is meant for code a struct’s author
writes, such as a field-by-field ==, so it is available only in the
file that declares T; anywhere else it is an error that points at
properties.
Type parameters
T— a struct declared in this file
Returns
- one member per field
reflect.cases
reflect.cases<E>(): List<Case>
The cases of enum E, in declaration order.
Type parameters
E— an enum
Returns
- one case per enum case
reflect.functions_with
reflect.functions_with<A>(): List<Function>
Every free function in the program that carries attribute A.
Type parameters
A— an attribute struct
Returns
- one function per function marked
[A]
reflect.is_struct
reflect.is_struct<T>(): bool
Whether T is a struct. A number, bool or char is not, nor is an
enum or a trait.
Type parameters
T— the type to test
Returns
- whether
Tis a struct
reflect.is_enum
reflect.is_enum<T>(): bool
Whether T is an enum.
Type parameters
T— the type to test
Returns
- whether
Tis an enum
reflect.is_trait
reflect.is_trait<T>(): bool
Whether T is a trait.
Type parameters
T— the type to test
Returns
- whether
Tis a trait
reflect.is_reference
reflect.is_reference<T>(): bool
Whether T is a reference type, whose slots can be null: a struct, or an
enum whose cases carry values, unless it is [Value].
Type parameters
T— the type to test
Returns
- whether
Tis a reference type
reflect.is_same
reflect.is_same<A, B>(): bool
Whether A and B are the same type, after aliases (int and i32
are). A member’s Type can be either argument:
reflect.is_same<field.Type, String>().
Type parameters
A— the first typeB— the second type
Returns
- whether they are the same type
reflect.has_attribute
reflect.has_attribute<T, A>(): bool
Whether struct or enum T carries attribute A. A member, case or
function has the same test as a method with one type argument
(field.has_attribute<A>()).
Type parameters
T— the struct or enumA— the attribute struct
Returns
- whether
TcarriesA
reflect.attribute
reflect.attribute<T, A>(): A
The value of attribute A on struct or enum T: each field of attribute
struct A by name (reflect.attribute<T, Deprecated>().message). It is
an error when T does not carry A. A member, case or function has the
same as a method with one type argument (field.attribute<A>()).
Type parameters
T— the struct or enumA— the attribute struct
Returns
- the attribute’s value
reflect.has_build_option
reflect.has_build_option(name: String): bool
Whether the build defines build option
name (-D NAME or -D NAME=value).
Parameters
name— the option’s name: a letter or_, then letters, digits and_
Returns
- whether it is defined
reflect.build_option
reflect.build_option(name: String): String
The value of build option name, as a
string (-D NAME alone is "1"). It is an error when the build does not
define the option.
Parameters
name— the option’s name
Returns
- the option’s value
reflect.build_option
reflect.build_option(name: String, default: String): String
The value of build option name, as a
string, or default when the build does not define it.
Parameters
name— the option’s namedefault— the value when the build does not define it
Returns
- the option’s value, else
default
reflect.print
reflect.print<T>(value: T)
Writes value as a note each time the compiler runs the call, on every
compile, including one that succeeds; in an unrolled loop the note says
which pass printed it. See
Seeing what a block does.
Type parameters
T— the value’s compile-time kind
Parameters
value— any compile-time value
reflect.error
reflect.error(message: String)
Stops the compile with an error at the call.
Parameters
message— the error’s text; an interpolated string can show the values that matter
reflect.assert
reflect.assert(condition: bool, message: String)
Stops the compile with an error at condition when it is false.
Parameters
condition— a compile-time conditionmessage— the error’s text
reflect.Member
struct reflect.Member
A field or property, from properties or fields. A compile-time value:
no program can name its type.
Fields
name
const name: String
Its name.
index
const index: int
Its position in the list, from 0.
is_last
const is_last: bool
Whether it is the last in the list.
type_name
const type_name: String
Its type, as a string.
Type
const Type: Type
Its type, usable wherever a type is written
(var copy: field.Type = ...).
is_const
const is_const: bool
Whether it is a const field.
is_property
const is_property: bool
Whether it is a property rather than a field.
Methods
has_attribute
has_attribute<A>(): bool
Whether it carries attribute A.
Type parameters
A— the attribute struct
Returns
- whether it carries
A
attribute
attribute<A>(): A
The value of attribute A on it; an error when it does not carry A.
Type parameters
A— the attribute struct
Returns
- the attribute’s value
get
get<T>(value: T): Type
Emits value.name, a read of this member of value.
Type parameters
T— the struct
Parameters
value— the struct to read from
Returns
- the member’s value
set
set<T>(value: T, x: Type)
Emits value.name = x, a store to this member of value.
Type parameters
T— the struct
Parameters
value— the struct to store intox— the value to store
reflect.Case
struct reflect.Case
An enum case, from cases. A compile-time value: no program can name its
type.
Fields
name
const name: String
Its name.
value
const value: int
Its integer value, or the tag of a case that carries values.
index
const index: int
Its position, from 0.
is_last
const is_last: bool
Whether it is the last case.
values
const values: List<Member>
The values it carries, as members, in order; empty when it carries
none. A value’s get(v) reads it from v, which must hold this case.
Methods
has_attribute
has_attribute<A>(): bool
Whether it carries attribute A.
Type parameters
A— the attribute struct
Returns
- whether it carries
A
attribute
attribute<A>(): A
The value of attribute A on it; an error when it does not carry A.
Type parameters
A— the attribute struct
Returns
- the attribute’s value
matches
matches<E>(v: E): bool
Emits a test of whether v holds this case.
Type parameters
E— the enum
Parameters
v— the enum value to test
Returns
- whether
vholds this case
get
get<E, TValues>(values: TValues): E
Emits the case made with values (case.get() for one that carries
none); at module scope, the case itself.
Type parameters
E— the enumTValues— the types of the values
Parameters
values— the values the case carries, one argument each
Returns
- the enum value
reflect.Function
struct reflect.Function
A function, from functions_with. A compile-time value: no program can
name its type.
Fields
name
const name: String
Its name.
Methods
has_attribute
has_attribute<A>(): bool
Whether it carries attribute A.
Type parameters
A— the attribute struct
Returns
- whether it carries
A
attribute
attribute<A>(): A
The value of attribute A on it; an error when it does not carry A.
Type parameters
A— the attribute struct
Returns
- the attribute’s value
call
call<R, TArguments>(arguments: TArguments): R
Emits a direct call of the function with arguments.
Type parameters
R— what the function returnsTArguments— the types of the arguments
Parameters
arguments— the call’s arguments, one argument each
Returns
- what the call returns
reflect.List
struct reflect.List<T>
What properties, fields, cases, functions_with and a case’s
values return. A compile-time for unrolls over it.
Type parameters
T— a member, case or function
Fields
count
const count: int
How many items it holds.
is_empty
const is_empty: bool
Whether it holds none.
Methods
operator []
[](i: int): T
The item at index i.
Parameters
i— its position, from 0
Returns
- the item
reflect.String
struct reflect.String
A compile-time string: a literal, a member’s name, or the result of
name, + or interpolation. It joins with + and interpolation.
Fields
length
const length: int
How many characters it holds.
is_empty
const is_empty: bool
Whether it holds none.
Methods
starts_with
starts_with(s: String): bool
Whether it starts with s.
Parameters
s— the prefix
Returns
- whether it starts with
s
ends_with
ends_with(s: String): bool
Whether it ends with s.
Parameters
s— the suffix
Returns
- whether it ends with
s
contains
contains(s: String): bool
Whether s occurs in it.
Parameters
s— the text to find
Returns
- whether it contains
s
operator +
+(rhs: String): String
A new compile-time string: this one followed by rhs.
Parameters
rhs— the string to append
Returns
- the joined string
Compiler built-ins
The types and functions the compiler provides itself: raw pointers, weak
references, atomics, checked arithmetic and the other __ intrinsics the
runtime is built on. Every program can use them without an import, but
most code should not: the runtime wraps each in a safe API (Atomic<T>,
Weak<T>, math.checked_add, …).
The compiler provides these itself; this file only declares them, with
[__BuiltIn], for this reference and the editor.
Source: runtime/builtins.pep
__ptr
struct __ptr<T>
A raw pointer to a T in memory the program manages itself, as C’s T*.
It is how the runtime and C interop reach memory directly; nothing checks
that it is valid.
p[i] reads or writes the ith element, p + n and p - n move by n
elements (bytes for __ptr<void>), and == and != compare addresses.
A pointer is true when it is not null (__null). as converts to and
from __ptr<void>; unsafe_cast converts to any other pointer or to an
integer. A string literal converts to __ptr<char>.
Type parameters
T— the type pointed at;voidfor untyped memory
Methods
operator []
[](index: ulong): T
The element index places after the one pointed at, as a place that
can be read or assigned.
Parameters
index— how many elements past the pointer
Returns
- the element
operator +
+(n: long): __ptr<T>
A pointer n elements further on (bytes for __ptr<void>).
Parameters
n— how many elements to move
Returns
- the moved pointer
operator -
-(n: long): __ptr<T>
A pointer n elements back (bytes for __ptr<void>).
Parameters
n— how many elements to move
Returns
- the moved pointer
operator ==
==(other: __ptr<T>): bool
Whether both point at the same address.
Parameters
other— the pointer to compare with
Returns
- whether the addresses are equal
operator !=
!=(other: __ptr<T>): bool
Whether they point at different addresses.
Parameters
other— the pointer to compare with
Returns
- whether the addresses differ
__weak
struct __weak<T>
A weak reference to a struct object, like C++’s std::weak_ptr<T>: it
keeps the object’s memory but not the object alive. Copies step the weak
count; make one with __weak_from, read it with __weak_upgrade. Use
Weak<T>, which wraps it.
Type parameters
T— the struct or trait type pointed at
__null
const __null: __ptr<void>
The null pointer or reference: a __ptr<T> that points nowhere, or an
empty slot of a struct type.
sizeof
sizeof<T>(): ulong
The size of T in bytes, as C’s sizeof; a struct reference is the size
of a pointer. Written sizeof<T> or sizeof<T>().
Type parameters
T— the type to measure
Returns
- its size in bytes
unsafe_cast
unsafe_cast<T, TFrom>(value: TFrom): T
Reinterprets value as a T: a pointer as another pointer or an integer,
an integer as a pointer, or a number as another of the same width. It
never applies to a reference-counted type. Prefer as, which checks the
conversion.
Type parameters
T— the type to reinterpret asTFrom— the type ofvalue
Parameters
value— the value to reinterpret
Returns
- the same bits as a
T
__caller_location
__caller_location(): __ptr<char>
The place of the call, as "file:line:col". As a parameter’s default
(location: __ptr<char> = __caller_location()) it is the place of the
call that left the parameter out, which assert and panic report.
Returns
- the location, a NUL-terminated string that lives as long as the program
__function_pointer
__function_pointer<F>(function: F): __ptr<void>
The C-callable address of function: a free function, a static method of
the enclosing struct, or this.m. It is how a C library is handed a
callback.
Type parameters
F— the function’s type
Parameters
function— the function, by name
Returns
- its address
__weak_from
__weak_from<T>(target: T): __weak<T>
Makes a weak reference to target’s object, stepping its weak count.
Type parameters
T— the struct or trait type
Parameters
target— the object; null makes a reference that is always expired
Returns
- the weak reference
__weak_upgrade
__weak_upgrade<T>(weak: __weak<T>): T
A strong reference to the object weak points at, or null once it has
been destroyed.
Type parameters
T— the struct or trait type
Parameters
weak— the weak reference
Returns
- a new reference to the object, or null
__weak_expired
__weak_expired<T>(weak: __weak<T>): bool
Whether the object weak points at has been destroyed (or weak is
null).
Type parameters
T— the struct or trait type
Parameters
weak— the weak reference
Returns
- whether the object is gone
__default
__default<T>(): T
The zero value of T: 0, false, null, a value struct with every field
zero, or an enum’s default case.
Type parameters
T— the type
Returns
- its zero value
__is_pod
__is_pod<T>(): bool
Whether T is plain data, with no reference counts to step (a number,
bool or pointer, say). Decided while compiling.
Type parameters
T— the type
Returns
- whether
Tis plain data
__refcount
__refcount<T>(value: T): uint
The reference count of value’s object; 0 for plain data or null. For
tests and debugging.
Type parameters
T— the type
Parameters
value— the value
Returns
- its strong count
__address
__address<T>(value: T): ulong
A hash of value’s identity: an object’s address, the fields of an
[Equatable] struct, or the bits of plain data.
Type parameters
T— the type
Parameters
value— the value to hash
Returns
- the hash
__copy
__copy<T>(value: T, copies: _Copies): T
A deep copy of value, made in the copy context copies so that an
object reached twice is copied once. copy(value) calls it.
Type parameters
T— the type
Parameters
value— the value to copycopies— the copy context
Returns
- the copy
__copy_object
__copy_object<T>(value: T): T
A field-by-field copy of struct object value, through its generated
__copy().
Type parameters
T— the struct
Parameters
value— the object to copy
Returns
- the copy
__census
__census<T>(slot: __ptr<T>, waiting: ulong): __ptr<T>
Under PEP_CHECK_SYNC, checks that a Mutex<T>’s value at slot is not
reachable from outside the lock; otherwise does nothing.
Type parameters
T— the locked type
Parameters
slot— where the value is storedwaiting— how many threads are waiting on the lock
Returns
slot
__construct_at
__construct_at<T>(slot: T, value: T)
Stores value into slot, a raw element (p[i]) known to be empty: it
takes a reference to value and does not release what the slot held.
Type parameters
T— the element type
Parameters
slot— the empty element, writtenp[i]value— the value to store
__atomic_load
__atomic_load<T>(pointer: __ptr<T>): T
Reads *pointer atomically, with acquire ordering. A struct value comes
with a new reference.
Type parameters
T— a number,bool, pointer or struct type
Parameters
pointer— where the value is
Returns
- the value
__atomic_store
__atomic_store<T>(pointer: __ptr<T>, value: T)
Writes value to *pointer atomically, with release ordering. A struct
value is swapped in, and the old one released.
Type parameters
T— a number,bool, pointer or struct type
Parameters
pointer— where to storevalue— the value to store
__atomic_cmpxchg
__atomic_cmpxchg<T>(pointer: __ptr<T>, expected: T, desired: T): bool
Replaces *pointer with desired if it holds expected, atomically
(acquire-release).
Type parameters
T— a number,bool, pointer or struct type
Parameters
pointer— where the value isexpected— the value it must holddesired— the value to store
Returns
- whether it was replaced
__atomic_fetch_add
__atomic_fetch_add<T>(pointer: __ptr<T>, value: T): T
Adds value to *pointer atomically.
Type parameters
T— an integer type
Parameters
pointer— where the value isvalue— the amount to add
Returns
- the value before the addition
__atomic_fetch_sub
__atomic_fetch_sub<T>(pointer: __ptr<T>, value: T): T
Subtracts value from *pointer atomically.
Type parameters
T— an integer type
Parameters
pointer— where the value isvalue— the amount to subtract
Returns
- the value before the subtraction
__atomic_fetch_and
__atomic_fetch_and<T>(pointer: __ptr<T>, value: T): T
Sets *pointer to its bitwise and with value, atomically.
Type parameters
T— an integer type
Parameters
pointer— where the value isvalue— the bits to keep
Returns
- the value before the change
__atomic_fetch_or
__atomic_fetch_or<T>(pointer: __ptr<T>, value: T): T
Sets *pointer to its bitwise or with value, atomically.
Type parameters
T— an integer type
Parameters
pointer— where the value isvalue— the bits to set
Returns
- the value before the change
__atomic_fetch_xor
__atomic_fetch_xor<T>(pointer: __ptr<T>, value: T): T
Sets *pointer to its bitwise exclusive or with value, atomically.
Type parameters
T— an integer type
Parameters
pointer— where the value isvalue— the bits to flip
Returns
- the value before the change
__wrapping_add
__wrapping_add<T>(lhs: T, rhs: T): T
lhs + rhs, wrapping around on overflow instead of panicking.
Type parameters
T— an integer type, taken fromlhs
Parameters
lhs— the first operandrhs— the second operand
Returns
- the sum, modulo the type’s range
__wrapping_sub
__wrapping_sub<T>(lhs: T, rhs: T): T
lhs - rhs, wrapping around on overflow instead of panicking.
Type parameters
T— an integer type, taken fromlhs
Parameters
lhs— the first operandrhs— the second operand
Returns
- the difference, modulo the type’s range
__wrapping_mul
__wrapping_mul<T>(lhs: T, rhs: T): T
lhs * rhs, wrapping around on overflow instead of panicking.
Type parameters
T— an integer type, taken fromlhs
Parameters
lhs— the first operandrhs— the second operand
Returns
- the product, modulo the type’s range
__add_overflows
__add_overflows<T>(lhs: T, rhs: T): bool
Whether lhs + rhs overflows lhs’s type.
Type parameters
T— an integer type, taken fromlhs
Parameters
lhs— the first operandrhs— the second operand
Returns
- whether the sum overflows
__sub_overflows
__sub_overflows<T>(lhs: T, rhs: T): bool
Whether lhs - rhs overflows lhs’s type.
Type parameters
T— an integer type, taken fromlhs
Parameters
lhs— the first operandrhs— the second operand
Returns
- whether the difference overflows
__mul_overflows
__mul_overflows<T>(lhs: T, rhs: T): bool
Whether lhs * rhs overflows lhs’s type.
Type parameters
T— an integer type, taken fromlhs
Parameters
lhs— the first operandrhs— the second operand
Returns
- whether the product overflows
__simd
struct __simd<T, N>
A vector of N lanes of the number type T, as LLVM’s <N x T>: what
simd.Vector<T, N> and simd.Mask<N> (with bool lanes) hold. The
__simd_ functions below compute on it, each lane at once. It is a value,
with no references inside.
Type parameters
T— the lane type: an integer, floating-point orbooltypeN— the lane count, written as a number (__simd<byte, 16>)
__simd_splat
__simd_splat<V>(value: T): V
A vector V with every lane value.
Type parameters
V— the vector type,__simd<T, N>
Parameters
value— the value of every lane
Returns
- the vector
__simd_add
__simd_add<V>(lhs: V, rhs: V): V
Adds lane-wise; integer lanes wrap around.
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- the lane-wise results
__simd_subtract
__simd_subtract<V>(lhs: V, rhs: V): V
Subtracts lane-wise; integer lanes wrap around.
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- the lane-wise results
__simd_multiply
__simd_multiply<V>(lhs: V, rhs: V): V
Multiplies lane-wise; integer lanes wrap around.
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- the lane-wise results
__simd_divide
__simd_divide<V>(lhs: V, rhs: V): V
Divides lane-wise. An integer lane of rhs that is 0, or a signed MIN / -1, is undefined: simd.Vector checks first.
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- the lane-wise results
__simd_remainder
__simd_remainder<V>(lhs: V, rhs: V): V
The lane-wise remainder, with the same undefined cases as __simd_divide.
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- the lane-wise results
__simd_and
__simd_and<V>(lhs: V, rhs: V): V
The lane-wise bitwise and (integer or bool lanes).
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- the lane-wise results
__simd_or
__simd_or<V>(lhs: V, rhs: V): V
The lane-wise bitwise or (integer or bool lanes).
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- the lane-wise results
__simd_xor
__simd_xor<V>(lhs: V, rhs: V): V
The lane-wise bitwise exclusive or (integer or bool lanes).
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- the lane-wise results
__simd_shift_left
__simd_shift_left<V>(vector: V, count: int): V
Shifts every integer lane left. A count of at least the lane width is
undefined: simd.Vector checks first.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the lanes to shiftcount— the bits to shift by, less than the lane width
Returns
- the shifted lanes
__simd_shift_right
__simd_shift_right<V>(vector: V, count: int): V
Shifts every integer lane right: arithmetic for signed lanes, logical for unsigned. A count of at least the lane width is undefined.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the lanes to shiftcount— the bits to shift by, less than the lane width
Returns
- the shifted lanes
__simd_negate
__simd_negate<V>(vector: V): V
Negates every lane; integer lanes wrap around.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the operand
Returns
- the lane-wise results
__simd_not
__simd_not<V>(vector: V): V
Flips every bit of every integer or bool lane.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the operand
Returns
- the lane-wise results
__simd_equal
__simd_equal<V>(lhs: V, rhs: V): __simd<bool, N>
Compares lane-wise for equality (ordered for floats).
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- a
boolvector of the results
__simd_not_equal
__simd_not_equal<V>(lhs: V, rhs: V): __simd<bool, N>
Compares lane-wise for inequality (true for a NaN lane).
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- a
boolvector of the results
__simd_less
__simd_less<V>(lhs: V, rhs: V): __simd<bool, N>
Compares lane-wise, signed or unsigned by the lane type.
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- a
boolvector of the results
__simd_less_equal
__simd_less_equal<V>(lhs: V, rhs: V): __simd<bool, N>
Compares lane-wise, signed or unsigned by the lane type.
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- a
boolvector of the results
__simd_greater
__simd_greater<V>(lhs: V, rhs: V): __simd<bool, N>
Compares lane-wise, signed or unsigned by the lane type.
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- a
boolvector of the results
__simd_greater_equal
__simd_greater_equal<V>(lhs: V, rhs: V): __simd<bool, N>
Compares lane-wise, signed or unsigned by the lane type.
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- a
boolvector of the results
__simd_select
__simd_select<V>(mask: __simd<bool, N>, if_true: V, if_false: V): V
Picks each lane from if_true or if_false by mask.
Type parameters
V— the vector type,__simd<T, N>
Parameters
mask— which lanes to take fromif_trueif_true— the lanes wheremaskis trueif_false— the lanes wheremaskis false
Returns
- the selected lanes
__simd_minimum
__simd_minimum<V>(lhs: V, rhs: V): V
The lane-wise minimum (llvm.minnum for floats: a NaN lane gives the
other).
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- the lane-wise results
__simd_maximum
__simd_maximum<V>(lhs: V, rhs: V): V
The lane-wise maximum (llvm.maxnum for floats).
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the first operandrhs— the second operand
Returns
- the lane-wise results
__simd_abs
__simd_abs<V>(vector: V): V
The lane-wise absolute value; the lowest signed value stays itself.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the operand
Returns
- the lane-wise results
__simd_rotate_left
__simd_rotate_left<V>(vector: V, count: int): V
Rotates every integer lane left (llvm.fshl).
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the lanes to rotatecount— the bits to rotate by, modulo the lane width
Returns
- the rotated lanes
__simd_rotate_right
__simd_rotate_right<V>(vector: V, count: int): V
Rotates every integer lane right (llvm.fshr).
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the lanes to rotatecount— the bits to rotate by, modulo the lane width
Returns
- the rotated lanes
__simd_square_root
__simd_square_root<V>(vector: V): V
The square root of every float lane.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the operand
Returns
- the lane-wise results
__simd_floor
__simd_floor<V>(vector: V): V
Every float lane rounded down.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the operand
Returns
- the lane-wise results
__simd_ceiling
__simd_ceiling<V>(vector: V): V
Every float lane rounded up.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the operand
Returns
- the lane-wise results
__simd_sum
__simd_sum<V>(vector: V): T
The sum of the lanes: wrapping for integers, in lane order for floats.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the operand
Returns
- the sum
__simd_reduce_minimum
__simd_reduce_minimum<V>(vector: V): T
The smallest lane.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the operand
Returns
- the minimum
__simd_reduce_maximum
__simd_reduce_maximum<V>(vector: V): T
The largest lane.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the operand
Returns
- the maximum
__simd_any
__simd_any<V>(mask: V): bool
Whether any lane of a bool vector is true.
Type parameters
V— the vector type,__simd<T, N>
Parameters
mask— aboolvector
Returns
- whether a lane is true
__simd_all
__simd_all<V>(mask: V): bool
Whether every lane of a bool vector is true.
Type parameters
V— the vector type,__simd<T, N>
Parameters
mask— aboolvector
Returns
- whether every lane is true
__simd_to_bits
__simd_to_bits<V>(mask: V): ulong
Packs a bool vector into bits: bit i is lane i.
Type parameters
V— the vector type,__simd<T, N>
Parameters
mask— aboolvector of at most 64 lanes
Returns
- the lanes as bits
__simd_extract
__simd_extract<V>(vector: V, index: ulong): T
Reads one lane. An index of at least N gives an undefined value:
simd.Vector checks first.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the vectorindex— the lane, less thanN
Returns
- the lane’s value
__simd_insert
__simd_insert<V>(vector: V, index: ulong, value: T): V
Replaces one lane. An index of at least N is undefined: simd.Vector
checks first.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the vectorindex— the lane, less thanNvalue— the lane’s new value
Returns
- the vector with the lane replaced
__simd_shuffle
__simd_shuffle<V>(vector: V, indices: __simd<int, N>): V
Picks lanes by index, each taken modulo N. Constant indices fold to one
shufflevector.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the lanes to pick fromindices— the lane to pick for each lane, moduloN
Returns
- the picked lanes
__simd_reverse
__simd_reverse<V>(vector: V): V
The lanes in reverse order.
Type parameters
V— the vector type,__simd<T, N>
Parameters
vector— the operand
Returns
- the lane-wise results
__simd_interleave_low
__simd_interleave_low<V>(lhs: V, rhs: V): V
Interleaves the low halves: lhs[0], rhs[0], lhs[1], ....
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the even lanes’ sourcerhs— the odd lanes’ source
Returns
- the interleaved lanes
__simd_interleave_high
__simd_interleave_high<V>(lhs: V, rhs: V): V
Interleaves the high halves: lhs[N / 2], rhs[N / 2], ....
Type parameters
V— the vector type,__simd<T, N>
Parameters
lhs— the even lanes’ sourcerhs— the odd lanes’ source
Returns
- the interleaved lanes
__simd_load
__simd_load<V>(pointer: __ptr<T>): V
Reads N values from memory aligned to T.
Type parameters
V— the vector type,__simd<T, N>
Parameters
pointer— the first ofNvalues
Returns
- the values
__simd_load_aligned
__simd_load_aligned<V>(pointer: __ptr<T>): V
Reads N values from memory aligned to the vector’s size.
Type parameters
V— the vector type,__simd<T, N>
Parameters
pointer— the first ofNvalues, aligned to the vector’s size
Returns
- the values
__simd_store
__simd_store<V>(pointer: __ptr<T>, vector: V)
Writes the lanes to memory aligned to T.
Type parameters
V— the vector type,__simd<T, N>
Parameters
pointer— the first ofNvaluesvector— the lanes to write
__simd_store_aligned
__simd_store_aligned<V>(pointer: __ptr<T>, vector: V)
Writes the lanes to memory aligned to the vector’s size.
Type parameters
V— the vector type,__simd<T, N>
Parameters
pointer— the first ofNvalues, aligned to the vector’s sizevector— the lanes to write
__simd_convert
__simd_convert<R>(vector: __simd<U, N>): R
Converts every lane to R’s lane type, as as does; float to integer
saturates (llvm.fptosi.sat), and NaN gives 0.
Type parameters
R— the vector type,__simd<T, N>of the result
Parameters
vector— the lanes to convert
Returns
- the converted lanes
__simd_reinterpret
__simd_reinterpret<R>(vector: __simd<U, M>): R
Reads a vector’s bits as another vector type of the same size.
Type parameters
R— the vector type,__simd<T, N>of the result
Parameters
vector— the vector, the same size asR
Returns
- the same bits as
R
__simd_lanes
__simd_lanes<V>(): int
The lane count of V.
Type parameters
V— the vector type,__simd<T, N>
Returns
N
__simd_lane_bits
__simd_lane_bits<V>(): int
The width of V’s lane type in bits.
Type parameters
V— the vector type,__simd<T, N>
Returns
- the lane’s width in bits
__simd_is_signed
__simd_is_signed<V>(): bool
Whether V’s lanes are a signed integer type.
Type parameters
V— the vector type,__simd<T, N>
Returns
- whether the lanes are signed integers
Arguments and exit (deprecated)
The running program’s arguments and exit, kept for the deprecated
prelude arguments() and exit(); the process module has them now.
Source: runtime/process.pep
arguments
[Deprecated("use 'process.arguments' instead (import process)")] arguments(): Array<String>
Deprecated: use process.arguments instead.
Returns the program’s command-line arguments, as process.arguments()
does.
Returns
- the program name followed by its arguments
exit
[Deprecated("use 'process.exit' instead (import process)")] exit(code: int)
Deprecated: use process.exit instead.
Ends the program at once with status code, as process.exit(code)
does.
Parameters
code— the exit status: 0 for success, anything else for failure
String
Source: runtime/string.pep
String
struct String
A mutable, reference-counted string of UTF-8 text. A string literal
("text") is a String wherever a String is expected; its own type is
StringConstant.
Two measures describe a String, and each API says which one it uses:
- Code points (Unicode characters):
length,is_empty, the indexers[i],substr, and iteration. - Bytes of UTF-8:
size,find,starts_with, andends_with. The byte size is the authoritative length, so the data may contain NUL bytes. It is never derived from a terminator.
A String’s bytes are not guaranteed to be followed by a NUL. Only
c_str() guarantees one, so call it to hand the text to C; anything else
that reads the bytes must stop at size.
A String is a reference type: var b = a; shares one object, so
append, +=, and grow are visible through every reference. Use
copy() or + for an independent copy. Equality (==) compares
content.
A StringIterator panics when the String has been changed by append,
+=, or grow since the iterator was created, so modifying a String
while a for loop iterates it panics.
To build text from values, use an interpolated string
($"{name}: {count:>4}") or String.format("{0}: {1:>4}", name, count),
a compiler intrinsic whose braces name its arguments by position (see
the String interpolation chapter).
Example
var s = "héllo";
assert(s.length == 5, "5 code points");
assert(s.size == 6, "6 bytes: 'é' is 2");
s.append(" world");
assert(s.find("world") == 7, "find returns a byte offset");
assert(s[1] == "é", "the indexer counts code points");
for (var c in s) {
print(c); # one code point per line
}
Properties
length
var length: ulong { get; }
The number of Unicode code points. O(1).
See also
size
size
var size: ulong { get; }
The number of bytes of UTF-8. O(1).
See also
length
is_empty
var is_empty: bool { get; }
Whether the String has no code points. O(1).
Constructor
String()
construct(data: __ptr<char> = __null, size: ulong = 0, length: ulong = 0)
Creates a String by copying size bytes of data into a new
NUL-terminated buffer. With no arguments, it creates an empty String
with no buffer; its c_str() is still a valid empty C string.
When data is not __null and size is 0, data is treated as a
NUL-terminated C string:
- with
lengthnonzero,sizebecomes the byte length of its firstlengthcode points (0 if the C string is not valid UTF-8); - with
length0,sizebecomes the C string’s whole byte length.
When length is 0, it is counted from the bytes. A nonzero length is
trusted, not checked against the data. Runs in O(size).
Parameters
data— the bytes to copy;__nullfor an empty String (then pass no sizes, because they are stored as given)size— the number of bytes to copy fromdata; 0 treatsdataas a NUL-terminated C stringlength— the number of code points in those bytes
Example
var empty = String();
var s = "abc";
var copy = String(s.c_str(), s.size, s.length);
Static methods
length_of
static length_of(data: __ptr<char>, size: ulong): ulong
Returns the number of Unicode code points in the first size bytes at
data. Runs in O(size).
Returns 0 when data is __null, when size is 0, or when the bytes
are not valid UTF-8: any malformed or truncated sequence makes the
whole count 0.
Parameters
data— the UTF-8 bytes to measuresize— the number of bytes to measure
Returns
- the number of code points, or 0 for invalid UTF-8
Methods
bytes
bytes(): u8[]
Returns a new array holding the String’s UTF-8 bytes, without a terminating NUL. Runs in O(size).
Returns
- a new array of
sizebytes
Example
var data = "hi".bytes();
assert(data.count == 2 && data[0] == 104, "the bytes of 'h' and 'i'");
grow
grow(new_size: ulong)
Extends the byte buffer to new_size bytes and fills the new bytes
with zeros. Does nothing when new_size is not greater than size.
A low-level primitive for append. It updates size but not length,
so the zero bytes it adds are not counted as code points until the
caller overwrites them and fixes the length. When it grows the buffer,
it invalidates running iterators and may reallocate the buffer, which invalidates
pointers from c_str(). Runs in O(new_size).
Parameters
new_size— the new byte length
append
append(rhs: String)
Appends the content of rhs to this String in place, updating size
and length.
Always invalidates running iterators, even when rhs is empty, so
appending during iteration panics. It may reallocate the buffer. Every reference to
this String sees the change. Runs in O(size + rhs.size).
Parameters
rhs— the String to append; it is not modified
Example
var s = "TEST";
s.append("ING"); # s == "TESTING"
operator []
[](idx: int): String
Returns the code point at code-point index idx as a new
one-code-point String.
A negative idx counts from the end (s[-1] is the last code point).
length is added until the index is non-negative, so any negative
index wraps, and s[-7] of a 3-code-point String is s[2]. Runs in
O(idx) because it walks the UTF-8 bytes. Use for (var c in s) to
visit every code point in O(size).
Parameters
idx— the code-point index; negative counts from the end
Returns
- a new String that holds the one code point
Panics
- if
idxis not less thanlengthafter the negative index is normalized, which includes any index into an empty String
Example
var s = "TEST";
assert(s[0] == "T", "first");
assert(s[-1] == "T", "last");
operator ==
==(rhs: String): bool
Returns whether this String and rhs have the same content, compared
byte by byte. Strings with different length are unequal without
comparing bytes. Runs in O(size).
Parameters
rhs— the String to compare with
Returns
- true when both hold the same bytes
operator ==
==(rhs: StringConstant): bool
Returns whether this String holds the same bytes as the
StringConstant rhs, a literal included. Runs in O(size) and
copies nothing.
Parameters
rhs— the text to compare with
Returns
- true when both hold the same bytes
operator +=
+=(rhs: String): String
Appends rhs in place, like append, and returns this same String,
not a copy. s += t modifies the object that every reference to s
shares, and invalidates running iterators.
Parameters
rhs— the String to append
Returns
- this String, after appending
Example
var s = "a";
s += "b"; # s == "ab"
iterate
iterate(): StringIterator
Returns an iterator over the String’s code points, which makes String
Iterable<String>: for (var c in s) visits each code point as a
one-code-point String, the same value s[i] gives.
The iterator holds a reference to this String. The whole loop runs in O(size). Modifying the String during iteration makes the iterator panic.
Returns
- a new iterator, positioned before the first code point
Example
var n = 0;
for (var c in "héllo") {
n = n + 1; # 5 iterations
}
c_str
c_str(): __ptr<char>
Returns the String’s buffer as a NUL-terminated C string.
This is the one API that guarantees a NUL after the content: a String
is not otherwise guaranteed to be NUL-terminated, so code that needs
the terminator calls c_str() rather than relying on the buffer.
The pointer is owned by the String. It is valid until the String is
destroyed or append, +=, or grow reallocates it. It is never
__null: a String built with no data (String()) returns a constant
empty C string, so it can go to C as it is. C functions stop at the
first NUL byte in the content.
Returns
- the String’s NUL-terminated byte buffer
copy
copy(): String
Returns a new String with the same content and its own buffer, so later changes to either String do not affect the other. Runs in O(size).
This is also how copy(value) copies a String (the Copyable<String>
implementation).
Returns
- an independent copy of this String
Example
var a = "abc";
var b = a.copy();
b.append("d"); # a is still "abc"
clone
[Deprecated("use 'copy' instead", "copy")] clone(): String
Deprecated: use copy instead. Returns a new String with the same
content and its own buffer.
Returns
- an independent copy of this String
find
find(needle: String, from: ulong = 0): long
Returns the byte offset of the first occurrence of needle, or -1
when it does not occur.
The offset counts bytes, not code points. To use it with s[i] or
substr, convert it with String.length_of(s.c_str(), offset).
An empty needle is found at 0 unless this String is empty, in which
case the result is -1. The result is also -1 when needle is not valid
UTF-8, or when this String holds invalid UTF-8 before the first match.
Bytes past the match are not checked, so searching on from each match
(as split and replace do) reads the String once in all. The
search is the C library’s memmem, which compares many bytes per step
(Windows, which has none, jumps between matches of the first byte with
memchr).
With from, the search starts at that byte offset, which must be a
code point boundary (an offset find returned, plus a match’s size,
is one); an offset past the end, or inside a code point, finds
nothing. The result still counts from the start of the String.
Parameters
needle— the text to search forfrom— the byte offset to start searching at; 0 by default
Returns
- the byte offset of the first match at or after
from, or -1 when there is none
Example
var s = "TESTING";
assert(s.find("STI") == 2, "byte offset");
assert(s.find("xyz") == -1, "absent");
assert("a,b,c".find(",", 2) == 3, "the second comma");
rfind
rfind(needle: String): long
Returns the byte offset of the last place needle occurs in this
String, or -1 when it does not occur. An empty needle is found at
size. Runs in O(size * needle.size).
Parameters
needle— the text to search for
Returns
- the byte offset of the last match, or -1 when there is none
Example
assert("a/b/c".rfind("/") == 3, "the last slash");
assert("abc".rfind("x") == -1, "absent");
split
split(separator: String): String[]
Splits the String at every occurrence of separator and returns the
pieces between them, in order. Adjacent separators give an empty
piece, and so does a separator at either end: "a,,b".split(",") is
["a", "", "b"] and ",a".split(",") is ["", "a"]. A String
without separator, the empty String included, gives one piece: the
whole String. Runs in O(size * separator.size).
Parameters
separator— the text to split at
Returns
- a new Array with one new String per piece
Panics
- if
separatoris empty
Example
var parts = "a,b,,c".split(",");
assert(parts.count == 4, "four pieces");
assert(parts[2] == "", "an empty piece");
split_once
split_once(separator: String): (String, String)?
Splits the String at the first occurrence of separator: the text
before it and the text after it, or none when it does not occur.
Runs in O(size * separator.size).
Parameters
separator— the text to split at
Returns
- the pieces before and after the first
separator, or none
Panics
- if
separatoris empty
Example
var header = "Host: example.com".split_once(": ");
assert(header.is_value(), "has a colon");
var (name, value) = header.value();
assert(name == "Host" && value == "example.com", "split");
join
join(parts: String[]): String
Returns the pieces of parts joined into one String, with this String
between each two of them: ", ".join(names). No parts give an empty
String, and one part gives a copy of it. Runs in O(total size).
Parameters
parts— the pieces to join
Returns
- a new String
Example
var parts = Array<String>();
parts.append("a");
parts.append("b");
assert(", ".join(parts) == "a, b", "joined");
replace
replace(from: String, to: String): String
Returns a copy of this String with every occurrence of from replaced
by to, scanning left to right without overlaps: "aaa".replace("aa", "b") is "ba". Runs in O(size * from.size) plus the result’s
size.
Parameters
from— the text to replaceto— the text to put in its place
Returns
- a new String
Panics
- if
fromis empty
Example
assert("a\r\nb".replace("\r\n", "\n") == "a\nb", "line endings");
contains
contains(needle: String): bool
Returns whether needle occurs in this String (find(needle) >= 0).
Because of that, an empty String contains nothing, not even "".
Runs in O(size * needle.size).
Parameters
needle— the text to search for
Returns
- true when
needleoccurs
starts_with
starts_with(prefix: String): bool
Returns whether this String begins with the bytes of prefix. An
empty prefix always matches. Runs in O(prefix.size).
Parameters
prefix— the text to test for
Returns
- true when the first
prefix.sizebytes equalprefix
ends_with
ends_with(suffix: String): bool
Returns whether this String ends with the bytes of suffix. An empty
suffix always matches. Runs in O(suffix.size).
Parameters
suffix— the text to test for
Returns
- true when the last
suffix.sizebytes equalsuffix
substr
substr(start: ulong, count: ulong): String
Returns a new String of up to count code points starting at
code-point index start. Both arguments count code points, not bytes.
count is clamped to the code points that remain. When start is at
or past length, the result is an empty String. Runs in O(size).
Parameters
start— the code-point index of the first code point to copycount— the maximum number of code points to copy
Returns
- a new String that holds the selected code points
Example
var s = "TEST";
assert(s.substr(1, 2) == "ES", "middle");
assert(s.substr(0, 99) == "TEST", "count is clamped");
assert(s.substr(4, 5) == "", "start past the end");
operator !=
!=(rhs: String): bool
Returns whether this String and rhs differ in content, the negation
of ==. Runs in O(size).
Parameters
rhs— the String to compare with
Returns
- true when the contents differ
operator !=
!=(rhs: StringConstant): bool
Returns whether this String differs from the StringConstant rhs.
Parameters
rhs— the text to compare with
Returns
- true when the contents differ
operator +
+(rhs: String): String
Returns a new String that holds this String followed by rhs. Neither
operand is modified. Runs in O(size + rhs.size).
Parameters
rhs— the String to append to the copy
Returns
- a new concatenated String
Example
var greeting = "Hello, " + "world";
to_string
to_string(): String
Returns this String, so a String is ToString and can be
interpolated ($"name: {name}"). The result is the same object, not
a copy. O(1).
Returns
- this String
byte_at
byte_at(index: ulong): byte
Returns the byte at byte offset index of the UTF-8 data. O(1).
The offset counts bytes, not code points, so a byte inside a
multi-byte code point is returned as it is. Compare the result with a
character literal (s.byte_at(0) == '#').
Parameters
index— the byte offset, less thansize
Returns
- the byte at
index
Panics
- if
indexis not less thansize(“String byte_at index out of bounds”)
Example
var s = "a=b";
assert(s.byte_at(1) == '=', "second byte");
substr_bytes
substr_bytes(start: ulong, size: ulong): String
Returns a new String of the size bytes starting at byte offset
start. Both arguments count bytes, not code points, so offsets from
find can be used directly. Runs in O(size).
Parameters
start— the byte offset of the first byte to copysize— the number of bytes to copy
Returns
- a new String that holds those bytes
Panics
- if
start + sizeis greater thansize(“String substr_bytes range out of bounds”) - if
startorstart + sizefalls inside a multi-byte code point (“String substr_bytes splits a code point”)
Example
var s = "key=value";
var eq = s.find("=") as ulong;
assert(s.substr_bytes(0, eq) == "key", "before '='");
assert(s.substr_bytes(eq + 1, s.size - eq - 1) == "value", "after '='");
compare
compare(other: String): int
Compares this String with other byte by byte, for sorting. For
valid UTF-8 this is code-point order; a String that is a prefix of the
other orders first. Runs in O(min(size, other.size)).
Parameters
other— the String to compare with
Returns
- a negative number when this String orders before
other, 0 when both hold the same bytes, and a positive number when it orders after
Example
assert("apple".compare("banana") < 0, "a before b");
assert("ab".compare("abc") < 0, "a prefix orders first");
assert("x".compare("x") == 0, "equal");
operator <
<(other: String): bool
Returns whether this String orders before other, as compare does.
Parameters
other— the String to compare with
Returns
- true when
compare(other) < 0
operator <
<(other: StringConstant): bool
Returns whether this String orders before the StringConstant other,
byte by byte, a literal included. Copies nothing.
Parameters
other— the text to compare with
Returns
- true when the byte-by-byte order is negative
operator <=
<=(other: String): bool
Returns whether this String orders before other or equals it, as
compare does.
Parameters
other— the String to compare with
Returns
- true when
compare(other) <= 0
operator <=
<=(other: StringConstant): bool
Returns whether this String orders before or equals the StringConstant other,
byte by byte, a literal included. Copies nothing.
Parameters
other— the text to compare with
Returns
- true when the byte-by-byte order is not positive
operator >
>(other: String): bool
Returns whether this String orders after other, as compare does.
Parameters
other— the String to compare with
Returns
- true when
compare(other) > 0
operator >
>(other: StringConstant): bool
Returns whether this String orders after the StringConstant other,
byte by byte, a literal included. Copies nothing.
Parameters
other— the text to compare with
Returns
- true when the byte-by-byte order is positive
operator >=
>=(other: String): bool
Returns whether this String orders after other or equals it, as
compare does.
Parameters
other— the String to compare with
Returns
- true when
compare(other) >= 0
operator >=
>=(other: StringConstant): bool
Returns whether this String orders after or equals the StringConstant other,
byte by byte, a literal included. Copies nothing.
Parameters
other— the text to compare with
Returns
- true when the byte-by-byte order is not negative
trim_end
trim_end(): String
Returns a new String without the whitespace at the end: spaces, tabs, carriage returns, line feeds, vertical tabs and form feeds (ASCII only). Runs in O(size).
Returns
- a new String with the trailing whitespace removed
Example
assert("code; \r\n".trim_end() == "code;", "trailing whitespace");
assert(" indented".trim_end() == " indented", "leading kept");
trim_start
trim_start(): String
Returns a new String without the whitespace at the start: spaces, tabs, carriage returns, line feeds, vertical tabs and form feeds (ASCII only). Runs in O(size).
Returns
- a new String with the leading whitespace removed
Example
assert(" \tcode".trim_start() == "code", "leading whitespace");
trim
trim(): String
Returns a new String without the whitespace at either end, as
trim_start and trim_end do. Runs in O(size).
Returns
- a new String with the leading and trailing whitespace removed
Example
assert(" \t value \r\n".trim() == "value", "both ends");
lower
lower(): String
Returns a new String with the ASCII letters A to Z changed to a
to z. Every other character, including non-ASCII letters, is kept
as it is, so the result is valid UTF-8 of the same size. Runs in
O(size).
Returns
- a new String in lowercase
Example
assert("Content-Type".lower() == "content-type", "ASCII lowered");
assert("Ärger".lower() == "Ärger", "non-ASCII kept");
upper
upper(): String
Returns a new String with the ASCII letters a to z changed to A
to Z. Every other character, including non-ASCII letters, is kept
as it is, so the result is valid UTF-8 of the same size. Runs in
O(size).
Returns
- a new String in uppercase
Example
assert("get".upper() == "GET", "ASCII raised");
lines
lines(): String[]
Splits the String into lines and returns them without their line
endings. A line ends at "\n" or "\r\n". A final line with no
ending is included, but the end of the String after a final line
ending does not start another line, so "a\nb\n" and "a\nb" both
give ["a", "b"], and "" gives no lines. Runs in O(size).
Returns
- a new Array with one new String per line
Example
var lines = "first\r\nsecond\n\nfourth".lines();
assert(lines.count == 4, "four lines");
assert(lines[1] == "second", "\\r\\n removed");
assert(lines[2] == "", "an empty line");
repeat
repeat(count: ulong): String
Returns a new String that holds this String count times in a row.
count 0, or an empty String, gives an empty String. Runs in
O(size * count).
Parameters
count— how many copies to join
Returns
- a new String of
size * countbytes
Example
assert("ab".repeat(3) == "ababab", "three copies");
var indent = " ".repeat(8); # eight spaces
StringConstant
[Value] struct StringConstant
The text of a string literal: a pointer to its bytes, which never change
and are never freed, with their byte size and code-point length. A string
literal’s type is StringConstant, and so is a const with no written
type initialized from one (const NAME = "pepper";); where a String is
expected, the literal is a String (see the Strings chapter of the
book).
A StringConstant is a [Value] type: copying one copies the pointer
and steps no reference count, and any thread may read it. Its bytes
always end in a NUL byte, so c_str() goes to C as it is. It has
String’s read API, measured the same way (length in code points,
size and offsets in bytes); the methods that make new text return a
String. A StringConstant converts to a String wherever a String
is expected, as if String had an [Implicit] constructor from it: the
conversion, like String(text), copies the bytes. Where C expects a
char*, it passes its own bytes, as c_str() does.
Example
StringConstant name = "pepper";
assert(name.length == 6, "6 code points");
assert(name.starts_with("pep"), "the read API of String");
String copy = name; # a new String with its own bytes
copy.append("!");
Properties
length
var length: ulong { get; }
The number of Unicode code points. O(1).
See also
size
size
var size: ulong { get; }
The number of bytes of UTF-8, not counting the NUL terminator. O(1).
See also
length
is_empty
var is_empty: bool { get; }
Whether the text has no code points. O(1).
Constructor
StringConstant()
construct(data: __ptr<char> = __null, size: ulong = 0, length: ulong = 0)
Creates a StringConstant over the size bytes at data. The
compiler calls it for each string literal. The bytes must be followed
by a NUL byte and must never change or be freed while the program
runs; no other bytes meet that, so code outside the runtime has no
reason to call it with arguments. With no arguments, it is the empty
text.
Parameters
data— the constant NUL-terminated bytessize— the number of bytes before the NUL terminatorlength— the number of code points in those bytes
Methods
c_str
c_str(): __ptr<char>
Returns the text as a NUL-terminated C string, with no copy. It is
never __null, and it stays valid for the whole run of the program.
Returns
- the constant NUL-terminated bytes
to_string
to_string(): String
Returns a new String with a copy of the text. Converting a
StringConstant to a String, or calling String(text), calls it.
Runs in O(size).
Returns
- a new String with the same content
bytes
bytes(): u8[]
Returns a copy of the UTF-8 bytes in a new array, as String.bytes
does. Runs in O(size).
Returns
- a new array of
sizebytes
operator []
[](idx: int): String
Returns the code point at code-point index idx as a new
one-code-point String, as String’s indexer does. A negative idx
counts from the end. Runs in O(idx).
Parameters
idx— the code-point index; negative counts from the end
Returns
- a new String that holds the one code point
Panics
- if
idxis not less thanlengthafter the negative index is normalized
operator ==
==(rhs: StringConstant): bool
Returns whether two texts hold the same bytes, a literal rhs
included. Runs in O(size).
Parameters
rhs— the text to compare with
Returns
- true when both hold the same bytes
operator !=
!=(rhs: StringConstant): bool
Returns whether two texts differ.
Parameters
rhs— the text to compare with
Returns
- true when the bytes differ
operator ==
==(rhs: String): bool
Returns whether this text holds the same bytes as the String rhs.
Runs in O(size) and copies nothing.
Parameters
rhs— the String to compare with
Returns
- true when both hold the same bytes
operator !=
!=(rhs: String): bool
Returns whether this text differs from the String rhs.
Parameters
rhs— the String to compare with
Returns
- true when the bytes differ
operator +
+(rhs: String): String
Returns a new String: this text followed by rhs.
Parameters
rhs— the text to append
Returns
- a new String
find
find(needle: String, from: ulong = 0): long
Returns the byte offset of the first occurrence of needle at or
after byte offset from, or -1, as String.find does.
Parameters
needle— the text to search forfrom— the byte offset to start searching at; 0 by default
Returns
- the byte offset of the first match, or -1 when there is none
rfind
rfind(needle: String): long
Returns the byte offset of the last occurrence of needle, or -1, as
String.rfind does.
Parameters
needle— the text to search for
Returns
- the byte offset of the last match, or -1 when there is none
contains
contains(needle: String): bool
Returns whether needle occurs in the text.
Parameters
needle— the text to search for
Returns
- true when
find(needle)is not -1
starts_with
starts_with(prefix: String): bool
Returns whether the text starts with prefix. An empty prefix always
matches.
Parameters
prefix— the text to look for at the start
Returns
- true when the first bytes are
prefix’s
ends_with
ends_with(suffix: String): bool
Returns whether the text ends with suffix. An empty suffix always
matches.
Parameters
suffix— the text to look for at the end
Returns
- true when the last bytes are
suffix’s
substr
substr(start: ulong, count: ulong): String
Returns up to count code points starting at code-point index
start, as a new String, as String.substr does.
Parameters
start— the code-point index of the first code pointcount— the most code points to take
Returns
- a new String; empty when
startis past the end
byte_at
byte_at(index: ulong): byte
Returns the byte at byte offset index.
Parameters
index— the byte offset
Returns
- the byte at
index
Panics
- if
indexis not less thansize
substr_bytes
substr_bytes(start: ulong, size: ulong): String
Returns the size bytes from byte offset start as a new String, as
String.substr_bytes does.
Parameters
start— the byte offset of the first bytesize— the number of bytes
Returns
- a new String
Panics
- if the range is not inside the text
- if
startorstart + sizefalls inside a code point
compare
compare(other: String): int
Compares this text with other byte by byte, for sorting, as
String.compare does.
Parameters
other— the text to compare with
Returns
- a negative number, 0, or a positive number when this text sorts before, the same as, or after
other
operator <
<(other: StringConstant): bool
Returns whether this text sorts before other, byte by byte.
Parameters
other— the text to compare with
Returns
- true when the byte-by-byte order is negative
operator <
<(other: String): bool
Returns whether this text sorts before the String other, byte by byte.
Parameters
other— the String to compare with
Returns
- true when the byte-by-byte order is negative
operator <=
<=(other: StringConstant): bool
Returns whether this text sorts before or the same as other, byte by byte.
Parameters
other— the text to compare with
Returns
- true when the byte-by-byte order is not positive
operator <=
<=(other: String): bool
Returns whether this text sorts before or the same as the String other, byte by byte.
Parameters
other— the String to compare with
Returns
- true when the byte-by-byte order is not positive
operator >
>(other: StringConstant): bool
Returns whether this text sorts after other, byte by byte.
Parameters
other— the text to compare with
Returns
- true when the byte-by-byte order is positive
operator >
>(other: String): bool
Returns whether this text sorts after the String other, byte by byte.
Parameters
other— the String to compare with
Returns
- true when the byte-by-byte order is positive
operator >=
>=(other: StringConstant): bool
Returns whether this text sorts after or the same as other, byte by byte.
Parameters
other— the text to compare with
Returns
- true when the byte-by-byte order is not negative
operator >=
>=(other: String): bool
Returns whether this text sorts after or the same as the String other, byte by byte.
Parameters
other— the String to compare with
Returns
- true when the byte-by-byte order is not negative
trim_end
trim_end(): String
Returns a new String without the whitespace at the end.
Returns
- a new String with the trailing whitespace removed
trim_start
trim_start(): String
Returns a new String without the whitespace at the start.
Returns
- a new String with the leading whitespace removed
trim
trim(): String
Returns a new String without the whitespace at either end.
Returns
- a new String with the leading and trailing whitespace removed
lower
lower(): String
Returns a new String with the ASCII letters in lowercase.
Returns
- a new String
upper
upper(): String
Returns a new String with the ASCII letters in uppercase.
Returns
- a new String
lines
lines(): String[]
Returns the lines of the text, split at "\n" or "\r\n", as
String.lines does.
Returns
- a new array of Strings, without the line breaks
repeat
repeat(count: ulong): String
Returns a new String of count copies of the text.
Parameters
count— the number of copies
Returns
- a new String; empty when
countis 0
split
split(separator: String): String[]
Returns the pieces of the text between each separator, as
String.split does.
Parameters
separator— the text to split at
Returns
- a new array of Strings
Panics
- if
separatoris empty
split_once
split_once(separator: String): (String, String)?
Returns the text before and after the first separator, or Empty
when it does not occur, as String.split_once does.
Parameters
separator— the text to split at
Returns
- the two pieces, or
Empty
Panics
- if
separatoris empty
join
join(parts: String[]): String
Returns parts joined into a new String with this text between each
two.
Parameters
parts— the Strings to join
Returns
- a new String
replace
replace(from: String, to: String): String
Returns a new String with every occurrence of from replaced by
to, as String.replace does.
Parameters
from— the text to replaceto— the text to put in its place
Returns
- a new String
Panics
- if
fromis empty
iterate
iterate(): StringIterator
Returns an iterator over the code points, so for (var c in text)
visits each one as a one-code-point String.
Returns
- a new iterator, positioned before the first code point
to_string
to_string(value: long): String
Returns the decimal text of value, with a leading - when it is
negative.
to_string is overloaded for every integer type, so a call with any
integer picks its own overload.
Parameters
value— the number to convert
Returns
- a new String such as
"-42"
Example
assert(to_string(-42) == "-42", "negative");
var where = to_string(line) + ":" + to_string(column);
to_string
to_string(value: ulong): String
Returns the decimal text of value.
Parameters
value— the number to convert
Returns
- a new String such as
"18446744073709551615"
to_string
to_string(value: int): String
Returns the decimal text of value, with a leading - when it is
negative.
Parameters
value— the number to convert
Returns
- a new String such as
"7"
to_string
to_string(value: uint): String
Returns the decimal text of value.
Parameters
value— the number to convert
Returns
- a new String such as
"7"
to_string
to_string(value: short): String
Returns the decimal text of value, with a leading - when it is
negative.
Parameters
value— the number to convert
Returns
- a new String such as
"-7"
to_string
to_string(value: ushort): String
Returns the decimal text of value.
Parameters
value— the number to convert
Returns
- a new String such as
"7"
to_string
to_string(value: char): String
Returns the decimal text of value, with a leading - when it is
negative.
Parameters
value— the number to convert
Returns
- a new String such as
"-7"
to_string
to_string(value: byte): String
Returns the decimal text of value.
Parameters
value— the number to convert
Returns
- a new String such as
"7"
to_string
to_string(value: bool): String
Returns "true" or "false".
Parameters
value— the value to convert
Returns
- a new String,
"true"or"false"
to_string
to_string(value: double): String
Returns the shortest decimal text that reads back as exactly value,
as Swift prints a Double.
A whole number keeps a .0 ("3.0"), so the text always reads as a
floating-point number. Values from 1e-4 up to 1e16 print in decimal;
others use an exponent ("1e+16", "1.5e-05"). The special values are
"nan", "inf" and "-inf". For a fixed number of fraction digits,
interpolate with a precision instead: $"{value:.2}".
The digits come from the Ryu algorithm, which takes a fixed, small number of steps per value.
Parameters
value— the number to convert
Returns
- a new String such as
"0.1"or"-2.5"
Example
assert(to_string(0.1) == "0.1", "shortest");
assert(to_string(3.0) == "3.0", "keeps the point");
assert(to_string(1.0e20) == "1e+20", "exponent");
to_string
to_string(value: float): String
Returns the shortest decimal text that reads back as exactly value as
a float, laid out as to_string(double) lays it out.
Parameters
value— the number to convert
Returns
- a new String such as
"0.1"
Example
float third = 1.0 / 3.0;
assert(to_string(third) == "0.33333334", "a float's digits");
to_string
to_string<A, B>(value: (A, B)): String
Returns the text of a pair, "(1, x)": each element’s to_string
text, separated by ", ", in parentheses. A String element is its text
without quotes. Tuples of up to eight elements convert this way.
Type parameters
A— the type of the first elementB— the type of the second element
Parameters
value— the tuple to convert
Returns
- a new String such as
"(1, two)"
Example
assert(to_string((1, "two")) == "(1, two)", "a pair");
var nested = (1, (2.5, true));
assert($"{nested}" == "(1, (2.5, true))", "nested");
to_string
to_string<A, B, C>(value: (A, B, C)): String
Returns the text of a three-element tuple, as the pair overload does.
Type parameters
A— the type of the first elementB— the type of the second elementC— the type of the third element
Parameters
value— the tuple to convert
Returns
- a new String such as
"(1, 2, 3)"
to_string
to_string<A, B, C, D>(value: (A, B, C, D)): String
Returns the text of a four-element tuple, as the pair overload does.
Type parameters
A— the type of the first elementB— the type of the second elementC— the type of the third elementD— the type of the fourth element
Parameters
value— the tuple to convert
Returns
- a new String such as
"(1, 2, 3, 4)"
to_string
to_string<A, B, C, D, E>(value: (A, B, C, D, E)): String
Returns the text of a five-element tuple, as the pair overload does.
Type parameters
A— the type of the first elementB— the type of the second elementC— the type of the third elementD— the type of the fourth elementE— the type of the fifth element
Parameters
value— the tuple to convert
Returns
- a new String such as
"(1, 2, 3, 4, 5)"
to_string
to_string<A, B, C, D, E, F>(value: (A, B, C, D, E, F)): String
Returns the text of a six-element tuple, as the pair overload does.
Type parameters
A— the type of the first elementB— the type of the second elementC— the type of the third elementD— the type of the fourth elementE— the type of the fifth elementF— the type of the sixth element
Parameters
value— the tuple to convert
Returns
- a new String such as
"(1, 2, 3, 4, 5, 6)"
to_string
to_string<A, B, C, D, E, F, G>(value: (A, B, C, D, E, F, G)): String
Returns the text of a seven-element tuple, as the pair overload does.
Type parameters
A— the type of the first elementB— the type of the second elementC— the type of the third elementD— the type of the fourth elementE— the type of the fifth elementF— the type of the sixth elementG— the type of the seventh element
Parameters
value— the tuple to convert
Returns
- a new String such as
"(1, 2, 3, 4, 5, 6, 7)"
to_string
to_string<A, B, C, D, E, F, G, H>(value: (A, B, C, D, E, F, G, H)): String
Returns the text of an eight-element tuple, as the pair overload does.
Type parameters
A— the type of the first elementB— the type of the second elementC— the type of the third elementD— the type of the fourth elementE— the type of the fifth elementF— the type of the sixth elementG— the type of the seventh elementH— the type of the eighth element
Parameters
value— the tuple to convert
Returns
- a new String such as
"(1, 2, 3, 4, 5, 6, 7, 8)"
print(text: String)
Writes text and a line break to standard output.
Any other value prints through print(value), which writes its
to_string text. To combine values, interpolate them
(print($"total: {n}")), or pass a format string and arguments, which
prints what String.format makes of them (print("{0} of {1}", a, b)).
A string with no arguments after it is printed as written, braces
included.
Each line is flushed as it is printed, as Rust’s println! does, so the
output appears as the program runs even when standard output is a pipe
or a file (where C would buffer it until the program exits).
Parameters
text— the text to print
Example
print("hello");
var n = 3;
print($"n = {n}");
print("{0} + {1} = {2}", 1, 2, 1 + 2); # 1 + 2 = 3
print<T>(value: T)
Writes the text of value and a line break to standard output: what
$"{value}" would make of it, through to_string. That covers numbers,
bool (true or false), tuples, and any type with a to_string()
method or a to_string<T> override; another type is a compile error at
the argument.
Type parameters
T— the type ofvalue
Parameters
value— the value to print
Example
print(42); # 42
print(0.5); # 0.5
print((1, "two")); # (1, two)
to_string
to_string<T>(value: T): String
Returns the text of value by calling its to_string() method: the
overload for any type that is ToString.
Declare a concrete override for a type that has no to_string() method
of its own, or whose interpolated text should differ from it; like
hash<Point>, an override is used by the whole program, including
interpolated strings.
Type parameters
T— the type ofvalue; it must have ato_string()method unless an overrideto_string<T>exists
Parameters
value— the value to convert
Returns
- the String that
value.to_string()returns
Example
struct Point {
var x: int = 0;
var y: int = 0;
construct(this.x = 0, this.y = 0) {}
}
to_string<Point>(value: Point): String {
var x = value.x;
var y = value.y;
return $"({x}, {y})";
}
ToString
trait ToString
Something with a text form: a to_string() method returning a String.
An interpolated string ($"total: {a + b}") converts each value it holds
with to_string(value), so a struct that implements ToString can be
interpolated, as can the integers, bool, and String, which have
to_string overloads. String, StringBuilder and io.Path implement
it.
Example
struct Point {
var x: int = 0;
var y: int = 0;
construct(this.x = 0, this.y = 0) {}
}
extend Point with ToString {
to_string(): String {
return $"({x}, {y})";
}
}
var p = Point(1, 2);
assert($"p = {p}" == "p = (1, 2)", "interpolated");
Methods
to_string
to_string(): String
Returns the text of this value.
Returns
- a String
StringBuilder
struct StringBuilder
Builds a String from many pieces without copying the text built so far on every append.
String.append grows its buffer to the exact new size, so a long run of
appends can copy the text repeatedly. A StringBuilder doubles its buffer
when it is full, so appending is amortized O(piece size). Call
to_string() for the result; the builder can keep appending afterward.
Example
var out = StringBuilder();
out.append("line ").append(to_string(3)).append("\n");
var text = out.to_string(); # "line 3\n"
Properties
size
var size: ulong { get; }
The number of bytes of UTF-8 appended so far. O(1).
length
var length: ulong { get; }
The number of code points appended so far. O(1).
is_empty
var is_empty: bool { get; }
Whether nothing has been appended since the builder was created or last cleared. O(1).
Constructor
StringBuilder()
construct(capacity: ulong = 0)
Creates an empty StringBuilder, optionally with room for capacity
bytes before it has to grow.
Parameters
capacity— the number of bytes to allocate up front
Methods
append
append(text: String): StringBuilder
Appends the text of text and returns this builder, so calls can be
chained. Runs in amortized O(text.size).
Parameters
text— the String to append; it is not modified
Returns
- this StringBuilder
Example
var b = StringBuilder();
b.append("a").append("b"); # b.to_string() == "ab"
clear
clear()
Removes all the text and keeps the buffer for reuse. O(1).
to_string
to_string(): String
Returns a new String holding the text appended so far. The builder is unchanged. Runs in O(size).
Returns
- a new String
StringIterator
[Value] struct StringIterator
An Iterator<String> over a String’s code points, returned by
String.iterate() and used by for (var c in s).
Call next() to advance, then current() for the code point it moved
to. Both methods panic if the String has been modified since the
iterator was created. Create it with String.iterate(): a
default-constructed StringIterator() has no String to walk.
Example
var it = "ab".iterate();
while (it.next()) {
print(it.current()); # "a", then "b"
}
Methods
next
next(): bool
Advances to the next code point. Runs in O(1) per code point.
Returns false once it passes the end, and on every call after that. A malformed UTF-8 sequence is yielded, together with the rest of the bytes, as one final “code point”.
Returns
- true when positioned on a code point, false at the end
Panics
- if the String was modified since the iterator was created
current
current(): String
Returns the code point that the last successful next() moved to, as
a new one-code-point String.
Returns
- a new String that holds the current code point
Panics
- if the String was modified since the iterator was created
- if
next()has not been called yet - if
next()has returned false (the iterator is past the end)
Numbers
Reading numbers from text, and arithmetic that says what to do on overflow.
Every number type has a static parse: int.parse("42") gives an
int?, empty when the text is not a number of that type. The integer
types take a base from 2 to 36; float and double read decimal
notation. Each spelling of a type is the same type, so i32.parse is
int.parse.
Integer +, - and * panic on overflow. Each integer type also has
wrapping_add (the result modulo 2^bits), checked_add (none on
overflow), saturating_add (clamped to the type’s range) and
overflowing_add (the wrapped result and whether it overflowed), with
sub and mul forms of each.
Each integer type also has bit operations that compile to one
instruction on most CPUs (leading_zeros, trailing_zeros,
count_ones, count_zeros, swap_bytes, reverse_bits, rotate_left,
rotate_right), and min, max and, when signed, abs. float and
double have square_root, floor, ceiling, truncate, round,
abs, copy_sign, multiply_add, min and max, the trigonometric,
hyperbolic, exponential and logarithmic functions of C’s <math.h> with
their names spelled out (sine, arc_tangent2, logarithm10, …),
cube_root, hypotenuse, remainder, and is_nan, is_infinite,
is_finite and sign_bit. The math module has their constants
(math.PI).
Example
var port = int.parse("8080");
assert(port.is_value() && port.value() == 8080, "parsed");
assert(byte.parse("256").is_empty(), "too big for a byte");
int big = 2147483647;
assert(big.wrapping_add(1) == -2147483648, "wraps around");
assert(big.checked_add(1).is_empty(), "does not fit");
assert((40 as uint).trailing_zeros() == 3, "40 is 0b101000");
Source: runtime/number.pep
extend char
extend char
Static methods
parse
static parse(text: String, base: int = 10): char?
Parses text as a char (i8) written in base: an optional +
or -, then one or more digits, and nothing else. Digits past 9 are
the letters, either case. No spaces, prefixes (0x) or separators
are accepted.
Parameters
text— the text to parsebase— the base the digits are written in, from 2 to 36; 10 by default
Returns
- the value, or none when
textis not an integer inbaseor the value is outside -128 to 127
Panics
- if
baseis less than 2 or greater than 36
Example
assert(i8.parse("-128").value() == -128, "the lowest");
Methods
is_digit
is_digit(): bool
Whether this is an ASCII decimal digit, 0 to 9, like C’s
isdigit.
Like every classification method on char, it looks at ASCII only,
as C does in the “C” locale: a negative char (a byte of UTF-8 text
above 127) is never a digit, letter, space or punctuation. Unlike C’s
functions, it is defined for every value, negative ones included.
Returns
- true for
0to9
is_hex_digit
is_hex_digit(): bool
Whether this is an ASCII hexadecimal digit, 0 to 9, a to f or
A to F, like C’s isxdigit.
Returns
- true for a hexadecimal digit, either case
is_alphabetic
is_alphabetic(): bool
Whether this is an ASCII letter, a to z or A to Z, like C’s
isalpha.
Returns
- true for an ASCII letter
is_alphanumeric
is_alphanumeric(): bool
Whether this is an ASCII letter or digit, like C’s isalnum.
Returns
- true for
atoz,AtoZor0to9
is_upper
is_upper(): bool
Whether this is an ASCII uppercase letter, A to Z, like C’s
isupper.
Returns
- true for
AtoZ
is_lower
is_lower(): bool
Whether this is an ASCII lowercase letter, a to z, like C’s
islower.
Returns
- true for
atoz
is_whitespace
is_whitespace(): bool
Whether this is ASCII whitespace: a space, tab (\t), line feed
(\n), vertical tab, form feed or carriage return (\r), like C’s
isspace. String.trim strips the same set.
Returns
- true for one of the six whitespace characters
is_blank
is_blank(): bool
Whether this is a space or a tab, like C’s isblank.
Returns
- true for
' 'and'\t'
is_punctuation
is_punctuation(): bool
Whether this is ASCII punctuation: printable, but not a letter, digit
or space, like C’s ispunct. That is !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~.
Returns
- true for one of the 32 ASCII punctuation characters
is_graphic
is_graphic(): bool
Whether this is printable and not a space, ! to ~, like C’s
isgraph.
Returns
- true for a letter, digit or punctuation
is_printable
is_printable(): bool
Whether this is printable, a space through ~ (0x20 to 0x7E),
like C’s isprint.
Returns
- true for a space, letter, digit or punctuation
is_control
is_control(): bool
Whether this is an ASCII control character, 0x00 to 0x1F or
0x7F (delete), like C’s iscntrl.
Returns
- true for a control character
is_ascii
is_ascii(): bool
Whether this is ASCII, 0x00 to 0x7F, like C’s isascii.
Returns
- true for a value from 0 to 127
to_upper
to_upper(): char
Returns the uppercase letter for an ASCII lowercase letter, and this
value unchanged for anything else, like C’s toupper.
Returns
AtoZforatoz, otherwise this value
to_lower
to_lower(): char
Returns the lowercase letter for an ASCII uppercase letter, and this
value unchanged for anything else, like C’s tolower.
Returns
atozforAtoZ, otherwise this value
digit_value
digit_value(base: int = 10): int?
Returns the value of this digit in base: 0 to 9 are 0 to 9,
and the letters, either case, are 10 and up (a is 10, z is 35).
Parameters
base— the base the digit is written in, from 2 to 36; 10 by default
Returns
- the digit’s value, or none when this is not a digit in
base
Panics
- if
baseis less than 2 or greater than 36
wrapping_add
wrapping_add(other: char): char
Returns this + other modulo 2^8: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to add
Returns
- the sum, wrapped to a
char
checked_add
checked_add(other: char): char?
Returns this + other, or none when the sum does not fit a
char.
Parameters
other— the value to add
Returns
- the sum, or none on overflow
saturating_add
saturating_add(other: char): char
Returns this + other, clamped to the highest or lowest char on overflow.
Parameters
other— the value to add
Returns
- the sum, or the nearest
charto it
overflowing_add
overflowing_add(other: char): (char, bool)
Returns this + other modulo 2^8, and whether the operation
overflowed.
Parameters
other— the value to add
Returns
- the wrapped sum, and true when it overflowed
wrapping_sub
wrapping_sub(other: char): char
Returns this - other modulo 2^8: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to subtract
Returns
- the difference, wrapped to a
char
checked_sub
checked_sub(other: char): char?
Returns this - other, or none when the difference does not fit a
char.
Parameters
other— the value to subtract
Returns
- the difference, or none on overflow
saturating_sub
saturating_sub(other: char): char
Returns this - other, clamped to the highest or lowest char on overflow.
Parameters
other— the value to subtract
Returns
- the difference, or the nearest
charto it
overflowing_sub
overflowing_sub(other: char): (char, bool)
Returns this - other modulo 2^8, and whether the operation
overflowed.
Parameters
other— the value to subtract
Returns
- the wrapped difference, and true when it overflowed
wrapping_mul
wrapping_mul(other: char): char
Returns this * other modulo 2^8: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to multiply by
Returns
- the product, wrapped to a
char
checked_mul
checked_mul(other: char): char?
Returns this * other, or none when the product does not fit a
char.
Parameters
other— the value to multiply by
Returns
- the product, or none on overflow
saturating_mul
saturating_mul(other: char): char
Returns this * other, clamped to the highest or lowest char on overflow.
Parameters
other— the value to multiply by
Returns
- the product, or the nearest
charto it
overflowing_mul
overflowing_mul(other: char): (char, bool)
Returns this * other modulo 2^8, and whether the operation
overflowed.
Parameters
other— the value to multiply by
Returns
- the wrapped product, and true when it overflowed
leading_zeros
leading_zeros(): int
Returns the number of zero bits above the highest set bit: 8 for
zero, 0 when the highest bit is set. One instruction on most CPUs
(llvm.ctlz).
Returns
- the count of leading zero bits, from 0 to 8
Example
assert(((1 as char)).leading_zeros() == 7, "one bit, at the bottom");
trailing_zeros
trailing_zeros(): int
Returns the number of zero bits below the lowest set bit: 8 for
zero. One instruction on most CPUs (llvm.cttz), so it finds the
first set bit of a mask without a loop.
Returns
- the count of trailing zero bits, from 0 to 8
Example
assert((8 as char).trailing_zeros() == 3, "8 is 0b1000");
count_ones
count_ones(): int
Returns the number of bits that are set (the population count,
llvm.ctpop).
Returns
- the count of one bits, from 0 to 8
Example
assert((11 as char).count_ones() == 3, "three bits set");
count_zeros
count_zeros(): int
Returns the number of bits that are clear.
Returns
- the count of zero bits, from 0 to 8
Example
assert((11 as char).count_zeros() == 5, "three bits set");
swap_bytes
swap_bytes(): char
Returns this value with its bytes in reverse order: a one-byte value
is itself. Each integer type has this method; see int.swap_bytes.
Returns
- this value
reverse_bits
reverse_bits(): char
Returns this value with its bits in reverse order: the lowest bit
becomes the highest (llvm.bitreverse).
Returns
- the value with its 8 bits reversed
Example
assert(((1 as char)).reverse_bits().leading_zeros() == 0, "bottom to top");
rotate_left
rotate_left(count: int): char
Returns this value’s bits rotated left by count: the bits shifted
out at the top come back in at the bottom (llvm.fshl). count is
taken modulo 8, so a negative count rotates right.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert((9 as char).rotate_left(7) == (9 as char).rotate_right(1), "the same rotation");
rotate_right
rotate_right(count: int): char
Returns this value’s bits rotated right by count: the bits shifted
out at the bottom come back in at the top (llvm.fshr). count is
taken modulo 8, so a negative count rotates left.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert(((1 as char)).rotate_right(1) == ((1 as char)).rotate_left(7), "the bottom bit to the top");
min
min(other: char): char
Returns the smaller of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not greater thanother, elseother
Example
assert((3 as char).min(5) == 3, "smaller");
max
max(other: char): char
Returns the larger of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not less thanother, elseother
Example
assert((3 as char).max(5) == 5, "larger");
abs
abs(): char
Returns the absolute value: this without its sign. The lowest
value, -128, has no positive counterpart in a char, so it panics, as
its negation does.
Returns
-thiswhen this value is negative, elsethis
Panics
- if this value is -128 (“integer overflow in ‘char’ absolute value”)
Example
assert((-5 as char).abs() == 5, "negative");
extend byte
extend byte
Static methods
parse
static parse(text: String, base: int = 10): byte?
Parses text as a byte (u8) written in base: an optional +,
then one or more digits, and nothing else. Digits past 9 are the
letters, either case. No spaces, prefixes (0x) or separators are
accepted.
Parameters
text— the text to parsebase— the base the digits are written in, from 2 to 36; 10 by default
Returns
- the value, or none when
textis not an integer inbaseor the value is above 255 (a-sign is never accepted)
Panics
- if
baseis less than 2 or greater than 36
Example
assert(byte.parse("ff", 16).value() == 255, "hexadecimal");
Methods
is_digit
is_digit(): bool
Whether this is an ASCII decimal digit, 0 to 9, like C’s
isdigit.
Like every classification method on byte, it looks at ASCII only,
as C does in the “C” locale: a value above 127 is never a digit,
letter, space or punctuation.
Returns
- true for
0to9
Example
assert(('7' as byte).is_digit(), "a digit");
assert(!('x' as byte).is_digit(), "a letter");
is_hex_digit
is_hex_digit(): bool
Whether this is an ASCII hexadecimal digit, 0 to 9, a to f or
A to F, like C’s isxdigit.
Returns
- true for a hexadecimal digit, either case
is_alphabetic
is_alphabetic(): bool
Whether this is an ASCII letter, a to z or A to Z, like C’s
isalpha.
Returns
- true for an ASCII letter
is_alphanumeric
is_alphanumeric(): bool
Whether this is an ASCII letter or digit, like C’s isalnum.
Returns
- true for
atoz,AtoZor0to9
is_upper
is_upper(): bool
Whether this is an ASCII uppercase letter, A to Z, like C’s
isupper.
Returns
- true for
AtoZ
is_lower
is_lower(): bool
Whether this is an ASCII lowercase letter, a to z, like C’s
islower.
Returns
- true for
atoz
is_whitespace
is_whitespace(): bool
Whether this is ASCII whitespace: a space, tab (\t), line feed
(\n), vertical tab, form feed or carriage return (\r), like C’s
isspace. String.trim strips the same set.
Returns
- true for one of the six whitespace characters
is_blank
is_blank(): bool
Whether this is a space or a tab, like C’s isblank.
Returns
- true for
' 'and'\t'
is_punctuation
is_punctuation(): bool
Whether this is ASCII punctuation: printable, but not a letter, digit
or space, like C’s ispunct. That is !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~.
Returns
- true for one of the 32 ASCII punctuation characters
Example
assert((';' as byte).is_punctuation(), "punctuation");
assert(!(' ' as byte).is_punctuation(), "a space is not");
is_graphic
is_graphic(): bool
Whether this is printable and not a space, ! to ~, like C’s
isgraph.
Returns
- true for a letter, digit or punctuation
is_printable
is_printable(): bool
Whether this is printable, a space through ~ (0x20 to 0x7E),
like C’s isprint.
Returns
- true for a space, letter, digit or punctuation
is_control
is_control(): bool
Whether this is an ASCII control character, 0x00 to 0x1F or
0x7F (delete), like C’s iscntrl.
Returns
- true for a control character
is_ascii
is_ascii(): bool
Whether this is ASCII, 0x00 to 0x7F, like C’s isascii.
Returns
- true for a value below 128
to_upper
to_upper(): byte
Returns the uppercase letter for an ASCII lowercase letter, and this
value unchanged for anything else, like C’s toupper.
Returns
AtoZforatoz, otherwise this value
Example
assert(('q' as byte).to_upper() == 'Q', "raised");
assert(('?' as byte).to_upper() == '?', "kept");
to_lower
to_lower(): byte
Returns the lowercase letter for an ASCII uppercase letter, and this
value unchanged for anything else, like C’s tolower.
Returns
atozforAtoZ, otherwise this value
digit_value
digit_value(base: int = 10): int?
Returns the value of this digit in base: 0 to 9 are 0 to 9,
and the letters, either case, are 10 and up (a is 10, z is 35).
Parameters
base— the base the digit is written in, from 2 to 36; 10 by default
Returns
- the digit’s value, or none when this is not a digit in
base
Panics
- if
baseis less than 2 or greater than 36
Example
assert(('7' as byte).digit_value().value() == 7, "decimal");
assert(('F' as byte).digit_value(16).value() == 15, "hexadecimal");
assert(('9' as byte).digit_value(8).is_empty(), "not octal");
wrapping_add
wrapping_add(other: byte): byte
Returns this + other modulo 2^8: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to add
Returns
- the sum, wrapped to a
byte
checked_add
checked_add(other: byte): byte?
Returns this + other, or none when the sum does not fit a
byte.
Parameters
other— the value to add
Returns
- the sum, or none on overflow
saturating_add
saturating_add(other: byte): byte
Returns this + other, clamped to the highest byte on overflow.
Parameters
other— the value to add
Returns
- the sum, or the nearest
byteto it
overflowing_add
overflowing_add(other: byte): (byte, bool)
Returns this + other modulo 2^8, and whether the operation
overflowed.
Parameters
other— the value to add
Returns
- the wrapped sum, and true when it overflowed
wrapping_sub
wrapping_sub(other: byte): byte
Returns this - other modulo 2^8: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to subtract
Returns
- the difference, wrapped to a
byte
checked_sub
checked_sub(other: byte): byte?
Returns this - other, or none when the difference does not fit a
byte.
Parameters
other— the value to subtract
Returns
- the difference, or none on overflow
saturating_sub
saturating_sub(other: byte): byte
Returns this - other, clamped to 0 on overflow.
Parameters
other— the value to subtract
Returns
- the difference, or the nearest
byteto it
overflowing_sub
overflowing_sub(other: byte): (byte, bool)
Returns this - other modulo 2^8, and whether the operation
overflowed.
Parameters
other— the value to subtract
Returns
- the wrapped difference, and true when it overflowed
wrapping_mul
wrapping_mul(other: byte): byte
Returns this * other modulo 2^8: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to multiply by
Returns
- the product, wrapped to a
byte
checked_mul
checked_mul(other: byte): byte?
Returns this * other, or none when the product does not fit a
byte.
Parameters
other— the value to multiply by
Returns
- the product, or none on overflow
saturating_mul
saturating_mul(other: byte): byte
Returns this * other, clamped to the highest byte on overflow.
Parameters
other— the value to multiply by
Returns
- the product, or the nearest
byteto it
overflowing_mul
overflowing_mul(other: byte): (byte, bool)
Returns this * other modulo 2^8, and whether the operation
overflowed.
Parameters
other— the value to multiply by
Returns
- the wrapped product, and true when it overflowed
leading_zeros
leading_zeros(): int
Returns the number of zero bits above the highest set bit: 8 for
zero, 0 when the highest bit is set. One instruction on most CPUs
(llvm.ctlz).
Returns
- the count of leading zero bits, from 0 to 8
Example
assert(((1 as byte)).leading_zeros() == 7, "one bit, at the bottom");
trailing_zeros
trailing_zeros(): int
Returns the number of zero bits below the lowest set bit: 8 for
zero. One instruction on most CPUs (llvm.cttz), so it finds the
first set bit of a mask without a loop.
Returns
- the count of trailing zero bits, from 0 to 8
Example
assert((8 as byte).trailing_zeros() == 3, "8 is 0b1000");
count_ones
count_ones(): int
Returns the number of bits that are set (the population count,
llvm.ctpop).
Returns
- the count of one bits, from 0 to 8
Example
assert((11 as byte).count_ones() == 3, "three bits set");
count_zeros
count_zeros(): int
Returns the number of bits that are clear.
Returns
- the count of zero bits, from 0 to 8
Example
assert((11 as byte).count_zeros() == 5, "three bits set");
swap_bytes
swap_bytes(): byte
Returns this value with its bytes in reverse order: a one-byte value
is itself. Each integer type has this method; see int.swap_bytes.
Returns
- this value
reverse_bits
reverse_bits(): byte
Returns this value with its bits in reverse order: the lowest bit
becomes the highest (llvm.bitreverse).
Returns
- the value with its 8 bits reversed
Example
assert(((1 as byte)).reverse_bits().leading_zeros() == 0, "bottom to top");
rotate_left
rotate_left(count: int): byte
Returns this value’s bits rotated left by count: the bits shifted
out at the top come back in at the bottom (llvm.fshl). count is
taken modulo 8, so a negative count rotates right.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert((9 as byte).rotate_left(7) == (9 as byte).rotate_right(1), "the same rotation");
rotate_right
rotate_right(count: int): byte
Returns this value’s bits rotated right by count: the bits shifted
out at the bottom come back in at the top (llvm.fshr). count is
taken modulo 8, so a negative count rotates left.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert(((1 as byte)).rotate_right(1) == ((1 as byte)).rotate_left(7), "the bottom bit to the top");
min
min(other: byte): byte
Returns the smaller of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not greater thanother, elseother
Example
assert((3 as byte).min(5) == 3, "smaller");
max
max(other: byte): byte
Returns the larger of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not less thanother, elseother
Example
assert((3 as byte).max(5) == 5, "larger");
extend short
extend short
Static methods
parse
static parse(text: String, base: int = 10): short?
Parses text as a short (i16) written in base: an optional
+ or -, then one or more digits, and nothing else. Digits past 9
are the letters, either case. No spaces, prefixes (0x) or
separators are accepted.
Parameters
text— the text to parsebase— the base the digits are written in, from 2 to 36; 10 by default
Returns
- the value, or none when
textis not an integer inbaseor the value is outside -32768 to 32767
Panics
- if
baseis less than 2 or greater than 36
Example
assert(short.parse("-300").value() == -300, "decimal");
Methods
wrapping_add
wrapping_add(other: short): short
Returns this + other modulo 2^16: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to add
Returns
- the sum, wrapped to a
short
checked_add
checked_add(other: short): short?
Returns this + other, or none when the sum does not fit a
short.
Parameters
other— the value to add
Returns
- the sum, or none on overflow
saturating_add
saturating_add(other: short): short
Returns this + other, clamped to the highest or lowest short on overflow.
Parameters
other— the value to add
Returns
- the sum, or the nearest
shortto it
overflowing_add
overflowing_add(other: short): (short, bool)
Returns this + other modulo 2^16, and whether the operation
overflowed.
Parameters
other— the value to add
Returns
- the wrapped sum, and true when it overflowed
wrapping_sub
wrapping_sub(other: short): short
Returns this - other modulo 2^16: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to subtract
Returns
- the difference, wrapped to a
short
checked_sub
checked_sub(other: short): short?
Returns this - other, or none when the difference does not fit a
short.
Parameters
other— the value to subtract
Returns
- the difference, or none on overflow
saturating_sub
saturating_sub(other: short): short
Returns this - other, clamped to the highest or lowest short on overflow.
Parameters
other— the value to subtract
Returns
- the difference, or the nearest
shortto it
overflowing_sub
overflowing_sub(other: short): (short, bool)
Returns this - other modulo 2^16, and whether the operation
overflowed.
Parameters
other— the value to subtract
Returns
- the wrapped difference, and true when it overflowed
wrapping_mul
wrapping_mul(other: short): short
Returns this * other modulo 2^16: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to multiply by
Returns
- the product, wrapped to a
short
checked_mul
checked_mul(other: short): short?
Returns this * other, or none when the product does not fit a
short.
Parameters
other— the value to multiply by
Returns
- the product, or none on overflow
saturating_mul
saturating_mul(other: short): short
Returns this * other, clamped to the highest or lowest short on overflow.
Parameters
other— the value to multiply by
Returns
- the product, or the nearest
shortto it
overflowing_mul
overflowing_mul(other: short): (short, bool)
Returns this * other modulo 2^16, and whether the operation
overflowed.
Parameters
other— the value to multiply by
Returns
- the wrapped product, and true when it overflowed
leading_zeros
leading_zeros(): int
Returns the number of zero bits above the highest set bit: 16 for
zero, 0 when the highest bit is set. One instruction on most CPUs
(llvm.ctlz).
Returns
- the count of leading zero bits, from 0 to 16
Example
assert(((1 as short)).leading_zeros() == 15, "one bit, at the bottom");
trailing_zeros
trailing_zeros(): int
Returns the number of zero bits below the lowest set bit: 16 for
zero. One instruction on most CPUs (llvm.cttz), so it finds the
first set bit of a mask without a loop.
Returns
- the count of trailing zero bits, from 0 to 16
Example
assert((8 as short).trailing_zeros() == 3, "8 is 0b1000");
count_ones
count_ones(): int
Returns the number of bits that are set (the population count,
llvm.ctpop).
Returns
- the count of one bits, from 0 to 16
Example
assert((11 as short).count_ones() == 3, "three bits set");
count_zeros
count_zeros(): int
Returns the number of bits that are clear.
Returns
- the count of zero bits, from 0 to 16
Example
assert((11 as short).count_zeros() == 13, "three bits set");
swap_bytes
swap_bytes(): short
Returns this value with its bytes in reverse order (llvm.bswap), to
convert between little-endian and big-endian byte order.
Returns
- the value with its 2 bytes reversed
Example
assert((0x1234 as short).swap_bytes() == 0x3412, "bytes reversed");
reverse_bits
reverse_bits(): short
Returns this value with its bits in reverse order: the lowest bit
becomes the highest (llvm.bitreverse).
Returns
- the value with its 16 bits reversed
Example
assert(((1 as short)).reverse_bits().leading_zeros() == 0, "bottom to top");
rotate_left
rotate_left(count: int): short
Returns this value’s bits rotated left by count: the bits shifted
out at the top come back in at the bottom (llvm.fshl). count is
taken modulo 16, so a negative count rotates right.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert((9 as short).rotate_left(15) == (9 as short).rotate_right(1), "the same rotation");
rotate_right
rotate_right(count: int): short
Returns this value’s bits rotated right by count: the bits shifted
out at the bottom come back in at the top (llvm.fshr). count is
taken modulo 16, so a negative count rotates left.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert(((1 as short)).rotate_right(1) == ((1 as short)).rotate_left(15), "the bottom bit to the top");
min
min(other: short): short
Returns the smaller of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not greater thanother, elseother
Example
assert((3 as short).min(5) == 3, "smaller");
max
max(other: short): short
Returns the larger of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not less thanother, elseother
Example
assert((3 as short).max(5) == 5, "larger");
abs
abs(): short
Returns the absolute value: this without its sign. The lowest
value, -32768, has no positive counterpart in a short, so it panics, as
its negation does.
Returns
-thiswhen this value is negative, elsethis
Panics
- if this value is -32768 (“integer overflow in ‘short’ absolute value”)
Example
assert((-5 as short).abs() == 5, "negative");
extend ushort
extend ushort
Static methods
parse
static parse(text: String, base: int = 10): ushort?
Parses text as a ushort (u16) written in base: an optional
+, then one or more digits, and nothing else. Digits past 9 are the
letters, either case. No spaces, prefixes (0x) or separators are
accepted.
Parameters
text— the text to parsebase— the base the digits are written in, from 2 to 36; 10 by default
Returns
- the value, or none when
textis not an integer inbaseor the value is above 65535 (a-sign is never accepted)
Panics
- if
baseis less than 2 or greater than 36
Example
assert(ushort.parse("8080").value() == 8080, "a port");
Methods
wrapping_add
wrapping_add(other: ushort): ushort
Returns this + other modulo 2^16: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to add
Returns
- the sum, wrapped to a
ushort
checked_add
checked_add(other: ushort): ushort?
Returns this + other, or none when the sum does not fit a
ushort.
Parameters
other— the value to add
Returns
- the sum, or none on overflow
saturating_add
saturating_add(other: ushort): ushort
Returns this + other, clamped to the highest ushort on overflow.
Parameters
other— the value to add
Returns
- the sum, or the nearest
ushortto it
overflowing_add
overflowing_add(other: ushort): (ushort, bool)
Returns this + other modulo 2^16, and whether the operation
overflowed.
Parameters
other— the value to add
Returns
- the wrapped sum, and true when it overflowed
wrapping_sub
wrapping_sub(other: ushort): ushort
Returns this - other modulo 2^16: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to subtract
Returns
- the difference, wrapped to a
ushort
checked_sub
checked_sub(other: ushort): ushort?
Returns this - other, or none when the difference does not fit a
ushort.
Parameters
other— the value to subtract
Returns
- the difference, or none on overflow
saturating_sub
saturating_sub(other: ushort): ushort
Returns this - other, clamped to 0 on overflow.
Parameters
other— the value to subtract
Returns
- the difference, or the nearest
ushortto it
overflowing_sub
overflowing_sub(other: ushort): (ushort, bool)
Returns this - other modulo 2^16, and whether the operation
overflowed.
Parameters
other— the value to subtract
Returns
- the wrapped difference, and true when it overflowed
wrapping_mul
wrapping_mul(other: ushort): ushort
Returns this * other modulo 2^16: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to multiply by
Returns
- the product, wrapped to a
ushort
checked_mul
checked_mul(other: ushort): ushort?
Returns this * other, or none when the product does not fit a
ushort.
Parameters
other— the value to multiply by
Returns
- the product, or none on overflow
saturating_mul
saturating_mul(other: ushort): ushort
Returns this * other, clamped to the highest ushort on overflow.
Parameters
other— the value to multiply by
Returns
- the product, or the nearest
ushortto it
overflowing_mul
overflowing_mul(other: ushort): (ushort, bool)
Returns this * other modulo 2^16, and whether the operation
overflowed.
Parameters
other— the value to multiply by
Returns
- the wrapped product, and true when it overflowed
leading_zeros
leading_zeros(): int
Returns the number of zero bits above the highest set bit: 16 for
zero, 0 when the highest bit is set. One instruction on most CPUs
(llvm.ctlz).
Returns
- the count of leading zero bits, from 0 to 16
Example
assert(((1 as ushort)).leading_zeros() == 15, "one bit, at the bottom");
trailing_zeros
trailing_zeros(): int
Returns the number of zero bits below the lowest set bit: 16 for
zero. One instruction on most CPUs (llvm.cttz), so it finds the
first set bit of a mask without a loop.
Returns
- the count of trailing zero bits, from 0 to 16
Example
assert((8 as ushort).trailing_zeros() == 3, "8 is 0b1000");
count_ones
count_ones(): int
Returns the number of bits that are set (the population count,
llvm.ctpop).
Returns
- the count of one bits, from 0 to 16
Example
assert((11 as ushort).count_ones() == 3, "three bits set");
count_zeros
count_zeros(): int
Returns the number of bits that are clear.
Returns
- the count of zero bits, from 0 to 16
Example
assert((11 as ushort).count_zeros() == 13, "three bits set");
swap_bytes
swap_bytes(): ushort
Returns this value with its bytes in reverse order (llvm.bswap), to
convert between little-endian and big-endian byte order.
Returns
- the value with its 2 bytes reversed
Example
assert((0x1234 as ushort).swap_bytes() == 0x3412, "bytes reversed");
reverse_bits
reverse_bits(): ushort
Returns this value with its bits in reverse order: the lowest bit
becomes the highest (llvm.bitreverse).
Returns
- the value with its 16 bits reversed
Example
assert(((1 as ushort)).reverse_bits().leading_zeros() == 0, "bottom to top");
rotate_left
rotate_left(count: int): ushort
Returns this value’s bits rotated left by count: the bits shifted
out at the top come back in at the bottom (llvm.fshl). count is
taken modulo 16, so a negative count rotates right.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert((9 as ushort).rotate_left(15) == (9 as ushort).rotate_right(1), "the same rotation");
rotate_right
rotate_right(count: int): ushort
Returns this value’s bits rotated right by count: the bits shifted
out at the bottom come back in at the top (llvm.fshr). count is
taken modulo 16, so a negative count rotates left.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert(((1 as ushort)).rotate_right(1) == ((1 as ushort)).rotate_left(15), "the bottom bit to the top");
min
min(other: ushort): ushort
Returns the smaller of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not greater thanother, elseother
Example
assert((3 as ushort).min(5) == 3, "smaller");
max
max(other: ushort): ushort
Returns the larger of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not less thanother, elseother
Example
assert((3 as ushort).max(5) == 5, "larger");
extend int
extend int
Static methods
parse
static parse(text: String, base: int = 10): int?
Parses text as an int (i32) written in base: an optional +
or -, then one or more digits, and nothing else. Digits past 9 are
the letters, either case ("ff" in base 16 is 255). No spaces,
prefixes (0x) or separators are accepted.
Parameters
text— the text to parsebase— the base the digits are written in, from 2 to 36; 10 by default
Returns
- the value, or none when
textis not an integer inbaseor the value does not fit anint
Panics
- if
baseis less than 2 or greater than 36
Example
assert(int.parse("-42").value() == -42, "decimal");
assert(int.parse("ff", 16).value() == 255, "hexadecimal");
assert(int.parse("12x").is_empty(), "not a number");
Methods
wrapping_add
wrapping_add(other: int): int
Returns this + other modulo 2^32: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to add
Returns
- the sum, wrapped to a
int
checked_add
checked_add(other: int): int?
Returns this + other, or none when the sum does not fit a
int.
Parameters
other— the value to add
Returns
- the sum, or none on overflow
saturating_add
saturating_add(other: int): int
Returns this + other, clamped to the highest or lowest int on overflow.
Parameters
other— the value to add
Returns
- the sum, or the nearest
intto it
overflowing_add
overflowing_add(other: int): (int, bool)
Returns this + other modulo 2^32, and whether the operation
overflowed.
Parameters
other— the value to add
Returns
- the wrapped sum, and true when it overflowed
wrapping_sub
wrapping_sub(other: int): int
Returns this - other modulo 2^32: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to subtract
Returns
- the difference, wrapped to a
int
checked_sub
checked_sub(other: int): int?
Returns this - other, or none when the difference does not fit a
int.
Parameters
other— the value to subtract
Returns
- the difference, or none on overflow
saturating_sub
saturating_sub(other: int): int
Returns this - other, clamped to the highest or lowest int on overflow.
Parameters
other— the value to subtract
Returns
- the difference, or the nearest
intto it
overflowing_sub
overflowing_sub(other: int): (int, bool)
Returns this - other modulo 2^32, and whether the operation
overflowed.
Parameters
other— the value to subtract
Returns
- the wrapped difference, and true when it overflowed
wrapping_mul
wrapping_mul(other: int): int
Returns this * other modulo 2^32: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to multiply by
Returns
- the product, wrapped to a
int
checked_mul
checked_mul(other: int): int?
Returns this * other, or none when the product does not fit a
int.
Parameters
other— the value to multiply by
Returns
- the product, or none on overflow
saturating_mul
saturating_mul(other: int): int
Returns this * other, clamped to the highest or lowest int on overflow.
Parameters
other— the value to multiply by
Returns
- the product, or the nearest
intto it
overflowing_mul
overflowing_mul(other: int): (int, bool)
Returns this * other modulo 2^32, and whether the operation
overflowed.
Parameters
other— the value to multiply by
Returns
- the wrapped product, and true when it overflowed
leading_zeros
leading_zeros(): int
Returns the number of zero bits above the highest set bit: 32 for
zero, 0 when the highest bit is set. One instruction on most CPUs
(llvm.ctlz).
Returns
- the count of leading zero bits, from 0 to 32
Example
assert(((1 as int)).leading_zeros() == 31, "one bit, at the bottom");
trailing_zeros
trailing_zeros(): int
Returns the number of zero bits below the lowest set bit: 32 for
zero. One instruction on most CPUs (llvm.cttz), so it finds the
first set bit of a mask without a loop.
Returns
- the count of trailing zero bits, from 0 to 32
Example
assert((8 as int).trailing_zeros() == 3, "8 is 0b1000");
count_ones
count_ones(): int
Returns the number of bits that are set (the population count,
llvm.ctpop).
Returns
- the count of one bits, from 0 to 32
Example
assert((11 as int).count_ones() == 3, "three bits set");
count_zeros
count_zeros(): int
Returns the number of bits that are clear.
Returns
- the count of zero bits, from 0 to 32
Example
assert((11 as int).count_zeros() == 29, "three bits set");
swap_bytes
swap_bytes(): int
Returns this value with its bytes in reverse order (llvm.bswap), to
convert between little-endian and big-endian byte order.
Returns
- the value with its 4 bytes reversed
Example
assert((0x12345678 as int).swap_bytes() == 0x78563412, "bytes reversed");
reverse_bits
reverse_bits(): int
Returns this value with its bits in reverse order: the lowest bit
becomes the highest (llvm.bitreverse).
Returns
- the value with its 32 bits reversed
Example
assert(((1 as int)).reverse_bits().leading_zeros() == 0, "bottom to top");
rotate_left
rotate_left(count: int): int
Returns this value’s bits rotated left by count: the bits shifted
out at the top come back in at the bottom (llvm.fshl). count is
taken modulo 32, so a negative count rotates right.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert((9 as int).rotate_left(31) == (9 as int).rotate_right(1), "the same rotation");
rotate_right
rotate_right(count: int): int
Returns this value’s bits rotated right by count: the bits shifted
out at the bottom come back in at the top (llvm.fshr). count is
taken modulo 32, so a negative count rotates left.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert(((1 as int)).rotate_right(1) == ((1 as int)).rotate_left(31), "the bottom bit to the top");
min
min(other: int): int
Returns the smaller of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not greater thanother, elseother
Example
assert((3 as int).min(5) == 3, "smaller");
max
max(other: int): int
Returns the larger of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not less thanother, elseother
Example
assert((3 as int).max(5) == 5, "larger");
abs
abs(): int
Returns the absolute value: this without its sign. The lowest
value, -2147483648, has no positive counterpart in a int, so it panics, as
its negation does.
Returns
-thiswhen this value is negative, elsethis
Panics
- if this value is -2147483648 (“integer overflow in ‘int’ absolute value”)
Example
assert((-5 as int).abs() == 5, "negative");
extend uint
extend uint
Static methods
parse
static parse(text: String, base: int = 10): uint?
Parses text as a uint (u32) written in base: an optional +,
then one or more digits, and nothing else. Digits past 9 are the
letters, either case. No spaces, prefixes (0x) or separators are
accepted.
Parameters
text— the text to parsebase— the base the digits are written in, from 2 to 36; 10 by default
Returns
- the value, or none when
textis not an integer inbaseor the value does not fit auint(a-sign is never accepted)
Panics
- if
baseis less than 2 or greater than 36
Example
assert(uint.parse("4294967295").value() == 4294967295, "the highest");
Methods
wrapping_add
wrapping_add(other: uint): uint
Returns this + other modulo 2^32: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to add
Returns
- the sum, wrapped to a
uint
checked_add
checked_add(other: uint): uint?
Returns this + other, or none when the sum does not fit a
uint.
Parameters
other— the value to add
Returns
- the sum, or none on overflow
saturating_add
saturating_add(other: uint): uint
Returns this + other, clamped to the highest uint on overflow.
Parameters
other— the value to add
Returns
- the sum, or the nearest
uintto it
overflowing_add
overflowing_add(other: uint): (uint, bool)
Returns this + other modulo 2^32, and whether the operation
overflowed.
Parameters
other— the value to add
Returns
- the wrapped sum, and true when it overflowed
wrapping_sub
wrapping_sub(other: uint): uint
Returns this - other modulo 2^32: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to subtract
Returns
- the difference, wrapped to a
uint
checked_sub
checked_sub(other: uint): uint?
Returns this - other, or none when the difference does not fit a
uint.
Parameters
other— the value to subtract
Returns
- the difference, or none on overflow
saturating_sub
saturating_sub(other: uint): uint
Returns this - other, clamped to 0 on overflow.
Parameters
other— the value to subtract
Returns
- the difference, or the nearest
uintto it
overflowing_sub
overflowing_sub(other: uint): (uint, bool)
Returns this - other modulo 2^32, and whether the operation
overflowed.
Parameters
other— the value to subtract
Returns
- the wrapped difference, and true when it overflowed
wrapping_mul
wrapping_mul(other: uint): uint
Returns this * other modulo 2^32: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to multiply by
Returns
- the product, wrapped to a
uint
checked_mul
checked_mul(other: uint): uint?
Returns this * other, or none when the product does not fit a
uint.
Parameters
other— the value to multiply by
Returns
- the product, or none on overflow
saturating_mul
saturating_mul(other: uint): uint
Returns this * other, clamped to the highest uint on overflow.
Parameters
other— the value to multiply by
Returns
- the product, or the nearest
uintto it
overflowing_mul
overflowing_mul(other: uint): (uint, bool)
Returns this * other modulo 2^32, and whether the operation
overflowed.
Parameters
other— the value to multiply by
Returns
- the wrapped product, and true when it overflowed
leading_zeros
leading_zeros(): int
Returns the number of zero bits above the highest set bit: 32 for
zero, 0 when the highest bit is set. One instruction on most CPUs
(llvm.ctlz).
Returns
- the count of leading zero bits, from 0 to 32
Example
assert(((1 as uint)).leading_zeros() == 31, "one bit, at the bottom");
trailing_zeros
trailing_zeros(): int
Returns the number of zero bits below the lowest set bit: 32 for
zero. One instruction on most CPUs (llvm.cttz), so it finds the
first set bit of a mask without a loop.
Returns
- the count of trailing zero bits, from 0 to 32
Example
assert((8 as uint).trailing_zeros() == 3, "8 is 0b1000");
count_ones
count_ones(): int
Returns the number of bits that are set (the population count,
llvm.ctpop).
Returns
- the count of one bits, from 0 to 32
Example
assert((11 as uint).count_ones() == 3, "three bits set");
count_zeros
count_zeros(): int
Returns the number of bits that are clear.
Returns
- the count of zero bits, from 0 to 32
Example
assert((11 as uint).count_zeros() == 29, "three bits set");
swap_bytes
swap_bytes(): uint
Returns this value with its bytes in reverse order (llvm.bswap), to
convert between little-endian and big-endian byte order.
Returns
- the value with its 4 bytes reversed
Example
assert((0x12345678 as uint).swap_bytes() == 0x78563412, "bytes reversed");
reverse_bits
reverse_bits(): uint
Returns this value with its bits in reverse order: the lowest bit
becomes the highest (llvm.bitreverse).
Returns
- the value with its 32 bits reversed
Example
assert(((1 as uint)).reverse_bits().leading_zeros() == 0, "bottom to top");
rotate_left
rotate_left(count: int): uint
Returns this value’s bits rotated left by count: the bits shifted
out at the top come back in at the bottom (llvm.fshl). count is
taken modulo 32, so a negative count rotates right.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert((9 as uint).rotate_left(31) == (9 as uint).rotate_right(1), "the same rotation");
rotate_right
rotate_right(count: int): uint
Returns this value’s bits rotated right by count: the bits shifted
out at the bottom come back in at the top (llvm.fshr). count is
taken modulo 32, so a negative count rotates left.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert(((1 as uint)).rotate_right(1) == ((1 as uint)).rotate_left(31), "the bottom bit to the top");
min
min(other: uint): uint
Returns the smaller of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not greater thanother, elseother
Example
assert((3 as uint).min(5) == 3, "smaller");
max
max(other: uint): uint
Returns the larger of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not less thanother, elseother
Example
assert((3 as uint).max(5) == 5, "larger");
extend long
extend long
Static methods
parse
static parse(text: String, base: int = 10): long?
Parses text as a long (i64) written in base: an optional +
or -, then one or more digits, and nothing else. Digits past 9 are
the letters, either case. No spaces, prefixes (0x) or separators
are accepted.
Parameters
text— the text to parsebase— the base the digits are written in, from 2 to 36; 10 by default
Returns
- the value, or none when
textis not an integer inbaseor the value does not fit along
Panics
- if
baseis less than 2 or greater than 36
Example
assert(long.parse("-9000000000").value() == -9000000000, "decimal");
assert(long.parse("9223372036854775808").is_empty(), "too big");
Methods
wrapping_add
wrapping_add(other: long): long
Returns this + other modulo 2^64: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to add
Returns
- the sum, wrapped to a
long
checked_add
checked_add(other: long): long?
Returns this + other, or none when the sum does not fit a
long.
Parameters
other— the value to add
Returns
- the sum, or none on overflow
saturating_add
saturating_add(other: long): long
Returns this + other, clamped to the highest or lowest long on overflow.
Parameters
other— the value to add
Returns
- the sum, or the nearest
longto it
overflowing_add
overflowing_add(other: long): (long, bool)
Returns this + other modulo 2^64, and whether the operation
overflowed.
Parameters
other— the value to add
Returns
- the wrapped sum, and true when it overflowed
wrapping_sub
wrapping_sub(other: long): long
Returns this - other modulo 2^64: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to subtract
Returns
- the difference, wrapped to a
long
checked_sub
checked_sub(other: long): long?
Returns this - other, or none when the difference does not fit a
long.
Parameters
other— the value to subtract
Returns
- the difference, or none on overflow
saturating_sub
saturating_sub(other: long): long
Returns this - other, clamped to the highest or lowest long on overflow.
Parameters
other— the value to subtract
Returns
- the difference, or the nearest
longto it
overflowing_sub
overflowing_sub(other: long): (long, bool)
Returns this - other modulo 2^64, and whether the operation
overflowed.
Parameters
other— the value to subtract
Returns
- the wrapped difference, and true when it overflowed
wrapping_mul
wrapping_mul(other: long): long
Returns this * other modulo 2^64: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to multiply by
Returns
- the product, wrapped to a
long
checked_mul
checked_mul(other: long): long?
Returns this * other, or none when the product does not fit a
long.
Parameters
other— the value to multiply by
Returns
- the product, or none on overflow
saturating_mul
saturating_mul(other: long): long
Returns this * other, clamped to the highest or lowest long on overflow.
Parameters
other— the value to multiply by
Returns
- the product, or the nearest
longto it
overflowing_mul
overflowing_mul(other: long): (long, bool)
Returns this * other modulo 2^64, and whether the operation
overflowed.
Parameters
other— the value to multiply by
Returns
- the wrapped product, and true when it overflowed
leading_zeros
leading_zeros(): int
Returns the number of zero bits above the highest set bit: 64 for
zero, 0 when the highest bit is set. One instruction on most CPUs
(llvm.ctlz).
Returns
- the count of leading zero bits, from 0 to 64
Example
assert(((1 as long)).leading_zeros() == 63, "one bit, at the bottom");
trailing_zeros
trailing_zeros(): int
Returns the number of zero bits below the lowest set bit: 64 for
zero. One instruction on most CPUs (llvm.cttz), so it finds the
first set bit of a mask without a loop.
Returns
- the count of trailing zero bits, from 0 to 64
Example
assert((8 as long).trailing_zeros() == 3, "8 is 0b1000");
count_ones
count_ones(): int
Returns the number of bits that are set (the population count,
llvm.ctpop).
Returns
- the count of one bits, from 0 to 64
Example
assert((11 as long).count_ones() == 3, "three bits set");
count_zeros
count_zeros(): int
Returns the number of bits that are clear.
Returns
- the count of zero bits, from 0 to 64
Example
assert((11 as long).count_zeros() == 61, "three bits set");
swap_bytes
swap_bytes(): long
Returns this value with its bytes in reverse order (llvm.bswap), to
convert between little-endian and big-endian byte order.
Returns
- the value with its 8 bytes reversed
Example
assert((0x0102030405060708 as long).swap_bytes() == 0x0807060504030201, "bytes reversed");
reverse_bits
reverse_bits(): long
Returns this value with its bits in reverse order: the lowest bit
becomes the highest (llvm.bitreverse).
Returns
- the value with its 64 bits reversed
Example
assert(((1 as long)).reverse_bits().leading_zeros() == 0, "bottom to top");
rotate_left
rotate_left(count: int): long
Returns this value’s bits rotated left by count: the bits shifted
out at the top come back in at the bottom (llvm.fshl). count is
taken modulo 64, so a negative count rotates right.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert((9 as long).rotate_left(63) == (9 as long).rotate_right(1), "the same rotation");
rotate_right
rotate_right(count: int): long
Returns this value’s bits rotated right by count: the bits shifted
out at the bottom come back in at the top (llvm.fshr). count is
taken modulo 64, so a negative count rotates left.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert(((1 as long)).rotate_right(1) == ((1 as long)).rotate_left(63), "the bottom bit to the top");
min
min(other: long): long
Returns the smaller of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not greater thanother, elseother
Example
assert((3 as long).min(5) == 3, "smaller");
max
max(other: long): long
Returns the larger of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not less thanother, elseother
Example
assert((3 as long).max(5) == 5, "larger");
abs
abs(): long
Returns the absolute value: this without its sign. The lowest
value, -9223372036854775808, has no positive counterpart in a long, so it panics, as
its negation does.
Returns
-thiswhen this value is negative, elsethis
Panics
- if this value is -9223372036854775808 (“integer overflow in ‘long’ absolute value”)
Example
assert((-5 as long).abs() == 5, "negative");
extend ulong
extend ulong
Static methods
parse
static parse(text: String, base: int = 10): ulong?
Parses text as a ulong (u64) written in base: an optional
+, then one or more digits, and nothing else. Digits past 9 are the
letters, either case. No spaces, prefixes (0x) or separators are
accepted.
Parameters
text— the text to parsebase— the base the digits are written in, from 2 to 36; 10 by default
Returns
- the value, or none when
textis not an integer inbaseor the value does not fit aulong(a-sign is never accepted)
Panics
- if
baseis less than 2 or greater than 36
Example
assert(ulong.parse("ffffffffffffffff", 16).is_value(), "the highest");
Methods
wrapping_add
wrapping_add(other: ulong): ulong
Returns this + other modulo 2^64: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to add
Returns
- the sum, wrapped to a
ulong
checked_add
checked_add(other: ulong): ulong?
Returns this + other, or none when the sum does not fit a
ulong.
Parameters
other— the value to add
Returns
- the sum, or none on overflow
saturating_add
saturating_add(other: ulong): ulong
Returns this + other, clamped to the highest ulong on overflow.
Parameters
other— the value to add
Returns
- the sum, or the nearest
ulongto it
overflowing_add
overflowing_add(other: ulong): (ulong, bool)
Returns this + other modulo 2^64, and whether the operation
overflowed.
Parameters
other— the value to add
Returns
- the wrapped sum, and true when it overflowed
wrapping_sub
wrapping_sub(other: ulong): ulong
Returns this - other modulo 2^64: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to subtract
Returns
- the difference, wrapped to a
ulong
checked_sub
checked_sub(other: ulong): ulong?
Returns this - other, or none when the difference does not fit a
ulong.
Parameters
other— the value to subtract
Returns
- the difference, or none on overflow
saturating_sub
saturating_sub(other: ulong): ulong
Returns this - other, clamped to 0 on overflow.
Parameters
other— the value to subtract
Returns
- the difference, or the nearest
ulongto it
overflowing_sub
overflowing_sub(other: ulong): (ulong, bool)
Returns this - other modulo 2^64, and whether the operation
overflowed.
Parameters
other— the value to subtract
Returns
- the wrapped difference, and true when it overflowed
wrapping_mul
wrapping_mul(other: ulong): ulong
Returns this * other modulo 2^64: on overflow the result wraps
around instead of panicking. For hashes, checksums and random number
generators, which rely on wrapping.
Parameters
other— the value to multiply by
Returns
- the product, wrapped to a
ulong
checked_mul
checked_mul(other: ulong): ulong?
Returns this * other, or none when the product does not fit a
ulong.
Parameters
other— the value to multiply by
Returns
- the product, or none on overflow
saturating_mul
saturating_mul(other: ulong): ulong
Returns this * other, clamped to the highest ulong on overflow.
Parameters
other— the value to multiply by
Returns
- the product, or the nearest
ulongto it
overflowing_mul
overflowing_mul(other: ulong): (ulong, bool)
Returns this * other modulo 2^64, and whether the operation
overflowed.
Parameters
other— the value to multiply by
Returns
- the wrapped product, and true when it overflowed
leading_zeros
leading_zeros(): int
Returns the number of zero bits above the highest set bit: 64 for
zero, 0 when the highest bit is set. One instruction on most CPUs
(llvm.ctlz).
Returns
- the count of leading zero bits, from 0 to 64
Example
assert(((1 as ulong)).leading_zeros() == 63, "one bit, at the bottom");
trailing_zeros
trailing_zeros(): int
Returns the number of zero bits below the lowest set bit: 64 for
zero. One instruction on most CPUs (llvm.cttz), so it finds the
first set bit of a mask without a loop.
Returns
- the count of trailing zero bits, from 0 to 64
Example
assert((8 as ulong).trailing_zeros() == 3, "8 is 0b1000");
count_ones
count_ones(): int
Returns the number of bits that are set (the population count,
llvm.ctpop).
Returns
- the count of one bits, from 0 to 64
Example
assert((11 as ulong).count_ones() == 3, "three bits set");
count_zeros
count_zeros(): int
Returns the number of bits that are clear.
Returns
- the count of zero bits, from 0 to 64
Example
assert((11 as ulong).count_zeros() == 61, "three bits set");
swap_bytes
swap_bytes(): ulong
Returns this value with its bytes in reverse order (llvm.bswap), to
convert between little-endian and big-endian byte order.
Returns
- the value with its 8 bytes reversed
Example
assert((0x0102030405060708 as ulong).swap_bytes() == 0x0807060504030201, "bytes reversed");
reverse_bits
reverse_bits(): ulong
Returns this value with its bits in reverse order: the lowest bit
becomes the highest (llvm.bitreverse).
Returns
- the value with its 64 bits reversed
Example
assert(((1 as ulong)).reverse_bits().leading_zeros() == 0, "bottom to top");
rotate_left
rotate_left(count: int): ulong
Returns this value’s bits rotated left by count: the bits shifted
out at the top come back in at the bottom (llvm.fshl). count is
taken modulo 64, so a negative count rotates right.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert((9 as ulong).rotate_left(63) == (9 as ulong).rotate_right(1), "the same rotation");
rotate_right
rotate_right(count: int): ulong
Returns this value’s bits rotated right by count: the bits shifted
out at the bottom come back in at the top (llvm.fshr). count is
taken modulo 64, so a negative count rotates left.
Parameters
count— how many places to rotate
Returns
- the rotated value
Example
assert(((1 as ulong)).rotate_right(1) == ((1 as ulong)).rotate_left(63), "the bottom bit to the top");
min
min(other: ulong): ulong
Returns the smaller of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not greater thanother, elseother
Example
assert((3 as ulong).min(5) == 3, "smaller");
max
max(other: ulong): ulong
Returns the larger of this value and other.
Parameters
other— the value to compare with
Returns
thiswhen it is not less thanother, elseother
Example
assert((3 as ulong).max(5) == 5, "larger");
extend float
extend float
Static methods
parse
static parse(text: String): float?
Parses text as a float written in decimal notation: an optional
+ or -, digits with an optional . among them ("1.5", ".5",
"2."), then an optional exponent ("1e-3", "2.5E8"), and nothing
else. The value is the nearest float. No spaces, inf, nan or
hexadecimal are accepted.
Parameters
text— the text to parse
Returns
- the value, or none when
textis not decimal notation or its value is too large for afloat(one too small rounds to zero)
Example
assert(float.parse("0.25").value() == 0.25, "decimal");
assert(float.parse("1e39").is_empty(), "too big");
Methods
square_root
square_root(): float
Returns the square root (llvm.sqrt, one instruction on most CPUs):
NaN for a negative value, and -0.0 for -0.0.
Returns
- the non-negative square root
Example
assert((2.25 as float).square_root() == 1.5, "exact");
floor
floor(): float
Returns the largest whole number not greater than this value
(llvm.floor).
Returns
- the value rounded toward negative infinity
Example
assert((-1.5 as float).floor() == -2.0, "down");
ceiling
ceiling(): float
Returns the smallest whole number not less than this value
(llvm.ceil).
Returns
- the value rounded toward positive infinity
Example
assert((-1.5 as float).ceiling() == -1.0, "up");
truncate
truncate(): float
Returns this value without its fractional part (llvm.trunc).
Returns
- the value rounded toward zero
Example
assert((-1.5 as float).truncate() == -1.0, "toward zero");
round
round(): float
Returns the nearest whole number, halfway cases away from zero
(llvm.round).
Returns
- the rounded value
Example
assert((2.5 as float).round() == 3.0, "half away from zero");
abs
abs(): float
Returns the absolute value: this value with its sign bit cleared
(llvm.fabs).
Returns
- the value without its sign
Example
assert((-2.0 as float).abs() == 2.0, "negative");
copy_sign
copy_sign(sign: float): float
Returns this value’s magnitude with the sign of sign
(llvm.copysign).
Parameters
sign— the value whose sign the result takes
Returns
abs()of this value, negated whensignis negative
Example
assert((3.0 as float).copy_sign(-0.0) == -3.0, "negative zero is negative");
multiply_add
multiply_add(factor: float, addend: float): float
Returns this * factor + addend with a single rounding, as one fused
multiply-add (llvm.fma): more precise than the two operations, and
one instruction where the CPU has it.
Parameters
factor— the value to multiply byaddend— the value to add to the product
Returns
- the fused product and sum
Example
assert((2.0 as float).multiply_add(3.0, 1.0) == 7.0, "2 * 3 + 1");
min
min(other: float): float
Returns the smaller of this value and other; when one of them is
NaN, the other (llvm.minnum).
Parameters
other— the value to compare with
Returns
- the smaller value
Example
assert((1.5 as float).min(-2.0) == -2.0, "smaller");
max
max(other: float): float
Returns the larger of this value and other; when one of them is
NaN, the other (llvm.maxnum).
Parameters
other— the value to compare with
Returns
- the larger value
Example
assert((1.5 as float).max(-2.0) == 1.5, "larger");
sine
sine(): float
Returns the sine of this angle, in radians (llvm.sin).
Returns
- the sine, from -1 to 1; NaN for an infinity or NaN
Example
assert((0.0 as float).sine() == 0.0, "sine of zero");
cosine
cosine(): float
Returns the cosine of this angle, in radians (llvm.cos).
Returns
- the cosine, from -1 to 1; NaN for an infinity or NaN
Example
assert((0.0 as float).cosine() == 1.0, "cosine of zero");
tangent
tangent(): float
Returns the tangent of this angle, in radians (llvm.tan).
Returns
- the tangent; NaN for an infinity or NaN
Example
assert((0.0 as float).tangent() == 0.0, "tangent of zero");
arc_sine
arc_sine(): float
Returns the angle, in radians, whose sine is this value (llvm.asin).
Returns
- the angle, from -π/2 to π/2; NaN when this value is outside -1 to 1
Example
assert((0.0 as float).arc_sine() == 0.0, "arc sine of zero");
arc_cosine
arc_cosine(): float
Returns the angle, in radians, whose cosine is this value (llvm.acos).
Returns
- the angle, from 0 to π; NaN when this value is outside -1 to 1
Example
assert((1.0 as float).arc_cosine() == 0.0, "arc cosine of one");
arc_tangent
arc_tangent(): float
Returns the angle, in radians, whose tangent is this value (llvm.atan).
Returns
- the angle, from -π/2 to π/2
Example
assert((0.0 as float).arc_tangent() == 0.0, "arc tangent of zero");
arc_tangent2
arc_tangent2(x: float): float
Returns the angle, in radians, from the positive x axis to the point
(x, this value), using both signs to pick the quadrant: C’s
atan2(y, x) with this value as y (llvm.atan2).
Parameters
x— the x coordinate of the point
Returns
- the angle, from -π to π
Example
assert((0.0 as float).arc_tangent2(1.0) == 0.0, "along the positive x axis");
assert((1.0 as float).arc_tangent2(0.0) == (1.0 as float).arc_sine(), "straight up is π/2");
hyperbolic_sine
hyperbolic_sine(): float
Returns the hyperbolic sine of this value (llvm.sinh).
Returns
- the hyperbolic sine; an infinity when it is too large
Example
assert((0.0 as float).hyperbolic_sine() == 0.0, "hyperbolic sine of zero");
hyperbolic_cosine
hyperbolic_cosine(): float
Returns the hyperbolic cosine of this value (llvm.cosh).
Returns
- the hyperbolic cosine, at least 1; an infinity when it is too large
Example
assert((0.0 as float).hyperbolic_cosine() == 1.0, "hyperbolic cosine of zero");
hyperbolic_tangent
hyperbolic_tangent(): float
Returns the hyperbolic tangent of this value (llvm.tanh).
Returns
- the hyperbolic tangent, from -1 to 1
Example
assert((0.0 as float).hyperbolic_tangent() == 0.0, "hyperbolic tangent of zero");
arc_hyperbolic_sine
arc_hyperbolic_sine(): float
Returns the value whose hyperbolic sine is this value (C’s asinh).
Returns
- the inverse hyperbolic sine
Example
assert((0.0 as float).arc_hyperbolic_sine() == 0.0, "inverse of zero");
arc_hyperbolic_cosine
arc_hyperbolic_cosine(): float
Returns the non-negative value whose hyperbolic cosine is this value
(C’s acosh).
Returns
- the inverse hyperbolic cosine; NaN when this value is less than 1
Example
assert((1.0 as float).arc_hyperbolic_cosine() == 0.0, "inverse of one");
arc_hyperbolic_tangent
arc_hyperbolic_tangent(): float
Returns the value whose hyperbolic tangent is this value (C’s atanh).
Returns
- the inverse hyperbolic tangent; an infinity for -1 or 1, and NaN outside
Example
assert((0.0 as float).arc_hyperbolic_tangent() == 0.0, "inverse of zero");
exponential
exponential(): float
Returns e raised to this value (llvm.exp).
Returns
- e to the power of this value; an infinity when it is too large
Example
assert((0.0 as float).exponential() == 1.0, "e to the zero");
exponential2
exponential2(): float
Returns 2 raised to this value (llvm.exp2).
Returns
- 2 to the power of this value
Example
assert((3.0 as float).exponential2() == 8.0, "2 cubed");
exponential_minus_one
exponential_minus_one(): float
Returns e raised to this value, minus 1 (C’s expm1): precise even
when this value is near zero, where exponential() - 1 loses its digits.
Returns
exponential() - 1, computed without the loss
Example
assert((0.0 as float).exponential_minus_one() == 0.0, "e to the zero, minus one");
logarithm
logarithm(): float
Returns the natural logarithm (base e) of this value (llvm.log).
Returns
- the logarithm; negative infinity for zero, and NaN for a negative value
Example
assert((1.0 as float).logarithm() == 0.0, "log of one");
logarithm2
logarithm2(): float
Returns the base-2 logarithm of this value (llvm.log2).
Returns
- the logarithm; negative infinity for zero, and NaN for a negative value
Example
assert((8.0 as float).logarithm2() == 3.0, "2 cubed is 8");
logarithm10
logarithm10(): float
Returns the base-10 logarithm of this value (llvm.log10).
Returns
- the logarithm; negative infinity for zero, and NaN for a negative value
Example
assert((1000.0 as float).logarithm10() == 3.0, "10 cubed is 1000");
logarithm_one_plus
logarithm_one_plus(): float
Returns the natural logarithm of 1 plus this value (C’s log1p):
precise even when this value is near zero, where (1 + x).logarithm()
loses its digits.
Returns
- the logarithm of
1 + this; negative infinity for -1, and NaN below it
Example
assert((0.0 as float).logarithm_one_plus() == 0.0, "log of one");
cube_root
cube_root(): float
Returns the cube root (C’s cbrt), negative for a negative value.
Returns
- the cube root
Example
assert((-27.0 as float).cube_root() == -3.0, "a negative value has a cube root");
hypotenuse
hypotenuse(other: float): float
Returns (this * this + other * other).square_root(), the length of
the hypotenuse of a right triangle with sides this value and other
(C’s hypot), without overflowing or underflowing in between.
Parameters
other— the length of the other side
Returns
- the distance from the origin to (this,
other)
Example
assert((3.0 as float).hypotenuse(4.0) == 5.0, "a 3-4-5 triangle");
remainder
remainder(divisor: float): float
Returns the IEEE remainder of this value divided by divisor (C’s
remainder): this - n * divisor, where n is the quotient rounded to
the nearest whole number, ties to even. It is never larger than half of
divisor, and may be negative. % gives the remainder of the quotient
rounded toward zero instead, with the sign of this value.
Parameters
divisor— the value to divide by
Returns
- the remainder; NaN when
divisoris zero or this value is infinite
Example
assert((7.0 as float).remainder(2.0) == -1.0, "7 / 2 = 3.5 rounds to 4");
assert((7.0 as float) % 2.0 == 1.0, "% rounds toward zero");
is_nan
is_nan(): bool
Returns whether this value is NaN (not a number), the one value not equal to itself.
Returns
- true for NaN
Example
assert(!(1.0 as float).is_nan(), "a number");
assert(((0.0 as float) / 0.0).is_nan(), "zero over zero");
is_infinite
is_infinite(): bool
Returns whether this value is positive or negative infinity.
Returns
- true for an infinity
Example
assert(((1.0 as float) / 0.0).is_infinite(), "one over zero");
is_finite
is_finite(): bool
Returns whether this value is neither an infinity nor NaN.
Returns
- true for every other value
Example
assert((1.0 as float).is_finite() && !((1.0 as float) / 0.0).is_finite(), "finite and infinite");
sign_bit
sign_bit(): bool
Returns whether this value’s sign bit is set: true for every negative
value, -0.0, and a NaN with its sign bit set, which < 0 does not
tell apart.
Returns
- true when the sign bit is set
Example
assert((-0.0 as float).sign_bit() && !(0.0 as float).sign_bit(), "negative zero");
extend double
extend double
Static methods
parse
static parse(text: String): double?
Parses text as a double written in decimal notation: an optional
+ or -, digits with an optional . among them ("1.5", ".5",
"2."), then an optional exponent ("1e-3", "2.5E8"), and nothing
else. The value is the nearest double. No spaces, inf, nan or
hexadecimal are accepted.
Parameters
text— the text to parse
Returns
- the value, or none when
textis not decimal notation or its value is too large for adouble(one too small rounds to zero)
Example
assert(double.parse("-2.5e3").value() == -2500.0, "an exponent");
assert(double.parse("1.5.2").is_empty(), "not a number");
Methods
square_root
square_root(): double
Returns the square root (llvm.sqrt, one instruction on most CPUs):
NaN for a negative value, and -0.0 for -0.0.
Returns
- the non-negative square root
Example
assert((2.25 as double).square_root() == 1.5, "exact");
floor
floor(): double
Returns the largest whole number not greater than this value
(llvm.floor).
Returns
- the value rounded toward negative infinity
Example
assert((-1.5 as double).floor() == -2.0, "down");
ceiling
ceiling(): double
Returns the smallest whole number not less than this value
(llvm.ceil).
Returns
- the value rounded toward positive infinity
Example
assert((-1.5 as double).ceiling() == -1.0, "up");
truncate
truncate(): double
Returns this value without its fractional part (llvm.trunc).
Returns
- the value rounded toward zero
Example
assert((-1.5 as double).truncate() == -1.0, "toward zero");
round
round(): double
Returns the nearest whole number, halfway cases away from zero
(llvm.round).
Returns
- the rounded value
Example
assert((2.5 as double).round() == 3.0, "half away from zero");
abs
abs(): double
Returns the absolute value: this value with its sign bit cleared
(llvm.fabs).
Returns
- the value without its sign
Example
assert((-2.0 as double).abs() == 2.0, "negative");
copy_sign
copy_sign(sign: double): double
Returns this value’s magnitude with the sign of sign
(llvm.copysign).
Parameters
sign— the value whose sign the result takes
Returns
abs()of this value, negated whensignis negative
Example
assert((3.0 as double).copy_sign(-0.0) == -3.0, "negative zero is negative");
multiply_add
multiply_add(factor: double, addend: double): double
Returns this * factor + addend with a single rounding, as one fused
multiply-add (llvm.fma): more precise than the two operations, and
one instruction where the CPU has it.
Parameters
factor— the value to multiply byaddend— the value to add to the product
Returns
- the fused product and sum
Example
assert((2.0 as double).multiply_add(3.0, 1.0) == 7.0, "2 * 3 + 1");
min
min(other: double): double
Returns the smaller of this value and other; when one of them is
NaN, the other (llvm.minnum).
Parameters
other— the value to compare with
Returns
- the smaller value
Example
assert((1.5 as double).min(-2.0) == -2.0, "smaller");
max
max(other: double): double
Returns the larger of this value and other; when one of them is
NaN, the other (llvm.maxnum).
Parameters
other— the value to compare with
Returns
- the larger value
Example
assert((1.5 as double).max(-2.0) == 1.5, "larger");
sine
sine(): double
Returns the sine of this angle, in radians (llvm.sin).
Returns
- the sine, from -1 to 1; NaN for an infinity or NaN
Example
assert((0.0 as double).sine() == 0.0, "sine of zero");
cosine
cosine(): double
Returns the cosine of this angle, in radians (llvm.cos).
Returns
- the cosine, from -1 to 1; NaN for an infinity or NaN
Example
assert((0.0 as double).cosine() == 1.0, "cosine of zero");
tangent
tangent(): double
Returns the tangent of this angle, in radians (llvm.tan).
Returns
- the tangent; NaN for an infinity or NaN
Example
assert((0.0 as double).tangent() == 0.0, "tangent of zero");
arc_sine
arc_sine(): double
Returns the angle, in radians, whose sine is this value (llvm.asin).
Returns
- the angle, from -π/2 to π/2; NaN when this value is outside -1 to 1
Example
assert((0.0 as double).arc_sine() == 0.0, "arc sine of zero");
arc_cosine
arc_cosine(): double
Returns the angle, in radians, whose cosine is this value (llvm.acos).
Returns
- the angle, from 0 to π; NaN when this value is outside -1 to 1
Example
assert((1.0 as double).arc_cosine() == 0.0, "arc cosine of one");
arc_tangent
arc_tangent(): double
Returns the angle, in radians, whose tangent is this value (llvm.atan).
Returns
- the angle, from -π/2 to π/2
Example
assert((0.0 as double).arc_tangent() == 0.0, "arc tangent of zero");
arc_tangent2
arc_tangent2(x: double): double
Returns the angle, in radians, from the positive x axis to the point
(x, this value), using both signs to pick the quadrant: C’s
atan2(y, x) with this value as y (llvm.atan2).
Parameters
x— the x coordinate of the point
Returns
- the angle, from -π to π
Example
assert((0.0 as double).arc_tangent2(1.0) == 0.0, "along the positive x axis");
assert((1.0 as double).arc_tangent2(0.0) == (1.0 as double).arc_sine(), "straight up is π/2");
hyperbolic_sine
hyperbolic_sine(): double
Returns the hyperbolic sine of this value (llvm.sinh).
Returns
- the hyperbolic sine; an infinity when it is too large
Example
assert((0.0 as double).hyperbolic_sine() == 0.0, "hyperbolic sine of zero");
hyperbolic_cosine
hyperbolic_cosine(): double
Returns the hyperbolic cosine of this value (llvm.cosh).
Returns
- the hyperbolic cosine, at least 1; an infinity when it is too large
Example
assert((0.0 as double).hyperbolic_cosine() == 1.0, "hyperbolic cosine of zero");
hyperbolic_tangent
hyperbolic_tangent(): double
Returns the hyperbolic tangent of this value (llvm.tanh).
Returns
- the hyperbolic tangent, from -1 to 1
Example
assert((0.0 as double).hyperbolic_tangent() == 0.0, "hyperbolic tangent of zero");
arc_hyperbolic_sine
arc_hyperbolic_sine(): double
Returns the value whose hyperbolic sine is this value (C’s asinh).
Returns
- the inverse hyperbolic sine
Example
assert((0.0 as double).arc_hyperbolic_sine() == 0.0, "inverse of zero");
arc_hyperbolic_cosine
arc_hyperbolic_cosine(): double
Returns the non-negative value whose hyperbolic cosine is this value
(C’s acosh).
Returns
- the inverse hyperbolic cosine; NaN when this value is less than 1
Example
assert((1.0 as double).arc_hyperbolic_cosine() == 0.0, "inverse of one");
arc_hyperbolic_tangent
arc_hyperbolic_tangent(): double
Returns the value whose hyperbolic tangent is this value (C’s atanh).
Returns
- the inverse hyperbolic tangent; an infinity for -1 or 1, and NaN outside
Example
assert((0.0 as double).arc_hyperbolic_tangent() == 0.0, "inverse of zero");
exponential
exponential(): double
Returns e raised to this value (llvm.exp).
Returns
- e to the power of this value; an infinity when it is too large
Example
assert((0.0 as double).exponential() == 1.0, "e to the zero");
exponential2
exponential2(): double
Returns 2 raised to this value (llvm.exp2).
Returns
- 2 to the power of this value
Example
assert((3.0 as double).exponential2() == 8.0, "2 cubed");
exponential_minus_one
exponential_minus_one(): double
Returns e raised to this value, minus 1 (C’s expm1): precise even
when this value is near zero, where exponential() - 1 loses its digits.
Returns
exponential() - 1, computed without the loss
Example
assert((0.0 as double).exponential_minus_one() == 0.0, "e to the zero, minus one");
logarithm
logarithm(): double
Returns the natural logarithm (base e) of this value (llvm.log).
Returns
- the logarithm; negative infinity for zero, and NaN for a negative value
Example
assert((1.0 as double).logarithm() == 0.0, "log of one");
logarithm2
logarithm2(): double
Returns the base-2 logarithm of this value (llvm.log2).
Returns
- the logarithm; negative infinity for zero, and NaN for a negative value
Example
assert((8.0 as double).logarithm2() == 3.0, "2 cubed is 8");
logarithm10
logarithm10(): double
Returns the base-10 logarithm of this value (llvm.log10).
Returns
- the logarithm; negative infinity for zero, and NaN for a negative value
Example
assert((1000.0 as double).logarithm10() == 3.0, "10 cubed is 1000");
logarithm_one_plus
logarithm_one_plus(): double
Returns the natural logarithm of 1 plus this value (C’s log1p):
precise even when this value is near zero, where (1 + x).logarithm()
loses its digits.
Returns
- the logarithm of
1 + this; negative infinity for -1, and NaN below it
Example
assert((0.0 as double).logarithm_one_plus() == 0.0, "log of one");
cube_root
cube_root(): double
Returns the cube root (C’s cbrt), negative for a negative value.
Returns
- the cube root
Example
assert((-27.0 as double).cube_root() == -3.0, "a negative value has a cube root");
hypotenuse
hypotenuse(other: double): double
Returns (this * this + other * other).square_root(), the length of
the hypotenuse of a right triangle with sides this value and other
(C’s hypot), without overflowing or underflowing in between.
Parameters
other— the length of the other side
Returns
- the distance from the origin to (this,
other)
Example
assert((3.0 as double).hypotenuse(4.0) == 5.0, "a 3-4-5 triangle");
remainder
remainder(divisor: double): double
Returns the IEEE remainder of this value divided by divisor (C’s
remainder): this - n * divisor, where n is the quotient rounded to
the nearest whole number, ties to even. It is never larger than half of
divisor, and may be negative. % gives the remainder of the quotient
rounded toward zero instead, with the sign of this value.
Parameters
divisor— the value to divide by
Returns
- the remainder; NaN when
divisoris zero or this value is infinite
Example
assert((7.0 as double).remainder(2.0) == -1.0, "7 / 2 = 3.5 rounds to 4");
assert((7.0 as double) % 2.0 == 1.0, "% rounds toward zero");
is_nan
is_nan(): bool
Returns whether this value is NaN (not a number), the one value not equal to itself.
Returns
- true for NaN
Example
assert(!(1.0 as double).is_nan(), "a number");
assert(((0.0 as double) / 0.0).is_nan(), "zero over zero");
is_infinite
is_infinite(): bool
Returns whether this value is positive or negative infinity.
Returns
- true for an infinity
Example
assert(((1.0 as double) / 0.0).is_infinite(), "one over zero");
is_finite
is_finite(): bool
Returns whether this value is neither an infinity nor NaN.
Returns
- true for every other value
Example
assert((1.0 as double).is_finite() && !((1.0 as double) / 0.0).is_finite(), "finite and infinite");
sign_bit
sign_bit(): bool
Returns whether this value’s sign bit is set: true for every negative
value, -0.0, and a NaN with its sign bit set, which < 0 does not
tell apart.
Returns
- true when the sign bit is set
Example
assert((-0.0 as double).sign_bit() && !(0.0 as double).sign_bit(), "negative zero");
Comparable
Ordering: Comparable, the trait of types whose values sort in one
order, which every number type, char and String declare.
Source: runtime/compare.pep
Comparable
trait Comparable
A type whose values have one order: compare says whether a value sorts
before, with or after another.
Declaring it (extend Version with Comparable;) gives the type <,
<=, > and >= and the methods min, max and clamp, each
written in terms of compare, unless the type declares its own. A
generic function asks for an order with T: Comparable.
compare returns a negative number when this value sorts before
other, zero when the two sort together, and a positive number when it
sorts after. The order must be total and consistent: two calls with the
same values give the same answer, and a.compare(b) has the opposite
sign of b.compare(a). Map, Set and sorting rely on that.
float and double are Comparable with a total order: NaN sorts
after every number and with every other NaN, and -0.0 with 0.0.
Their < and > keep IEEE behavior (false whenever NaN takes part),
while compare, clamp and Map keys follow the total order.
Example
struct Version {
var major: int;
var minor: int;
construct(this.major = 0, this.minor = 0) {}
compare(other: Version): int {
var order = major.compare(other.major);
if (order != 0) {
return order;
}
return minor.compare(other.minor);
}
}
extend Version with Comparable;
assert(Version(1, 2) < Version(1, 10), "by minor");
assert(Version(2, 0).max(Version(1, 9)).major == 2, "max");
Methods
compare
compare(other: this): int
Returns the order of this value and other.
Parameters
other— the value to compare with
Returns
- a negative number, zero or a positive number as this value sorts before, with or after
other
operator <
<(other: this): bool
Returns whether this value sorts before other.
Parameters
other— the value to compare with
Returns
- true when
compare(other) < 0
operator <=
<=(other: this): bool
Returns whether this value sorts before or with other.
Parameters
other— the value to compare with
Returns
- true when
compare(other) <= 0
operator >
>(other: this): bool
Returns whether this value sorts after other.
Parameters
other— the value to compare with
Returns
- true when
compare(other) > 0
operator >=
>=(other: this): bool
Returns whether this value sorts after or with other.
Parameters
other— the value to compare with
Returns
- true when
compare(other) >= 0
min
min(other: this): this
Returns the one of this value and other that sorts first.
Parameters
other— the value to compare with
Returns
- this value when it sorts before or with
other, elseother
Example
assert("pear".min("apple") == "apple", "apple sorts first");
max
max(other: this): this
Returns the one of this value and other that sorts last.
Parameters
other— the value to compare with
Returns
- this value when it sorts after or with
other, elseother
Example
assert("pear".max("apple") == "pear", "pear sorts last");
clamp
clamp(low: this, high: this): this
Returns this value limited to the range from low to high.
Parameters
low— the lowest value to returnhigh— the highest value to return
Returns
lowwhen this value sorts before it,highwhen this value sorts after it, else this value
Panics
- if
lowsorts afterhigh(“clamp range is empty”)
Example
assert((15).clamp(0, 10) == 10, "too high");
assert((-3).clamp(0, 10) == 0, "too low");
assert((4).clamp(0, 10) == 4, "in range");
extend char
extend char with Comparable
char values sort by value.
Methods
compare
compare(other: char): int
Returns the order of this value and other: -1, 0 or 1 as this
value is less than, equal to or greater than other.
Parameters
other— the value to compare with
Returns
- -1, 0 or 1
extend byte
extend byte with Comparable
byte values sort by value.
Methods
compare
compare(other: byte): int
Returns the order of this value and other: -1, 0 or 1 as this
value is less than, equal to or greater than other.
Parameters
other— the value to compare with
Returns
- -1, 0 or 1
extend short
extend short with Comparable
short values sort by value.
Methods
compare
compare(other: short): int
Returns the order of this value and other: -1, 0 or 1 as this
value is less than, equal to or greater than other.
Parameters
other— the value to compare with
Returns
- -1, 0 or 1
extend ushort
extend ushort with Comparable
ushort values sort by value.
Methods
compare
compare(other: ushort): int
Returns the order of this value and other: -1, 0 or 1 as this
value is less than, equal to or greater than other.
Parameters
other— the value to compare with
Returns
- -1, 0 or 1
extend int
extend int with Comparable
int values sort by value.
Methods
compare
compare(other: int): int
Returns the order of this value and other: -1, 0 or 1 as this
value is less than, equal to or greater than other.
Parameters
other— the value to compare with
Returns
- -1, 0 or 1
extend uint
extend uint with Comparable
uint values sort by value.
Methods
compare
compare(other: uint): int
Returns the order of this value and other: -1, 0 or 1 as this
value is less than, equal to or greater than other.
Parameters
other— the value to compare with
Returns
- -1, 0 or 1
extend long
extend long with Comparable
long values sort by value.
Methods
compare
compare(other: long): int
Returns the order of this value and other: -1, 0 or 1 as this
value is less than, equal to or greater than other.
Parameters
other— the value to compare with
Returns
- -1, 0 or 1
extend ulong
extend ulong with Comparable
ulong values sort by value.
Methods
compare
compare(other: ulong): int
Returns the order of this value and other: -1, 0 or 1 as this
value is less than, equal to or greater than other.
Parameters
other— the value to compare with
Returns
- -1, 0 or 1
extend float
extend float with Comparable
float values sort by value, with NaN after every number.
Methods
compare
compare(other: float): int
Returns the order of this value and other in a total order: by
value, with NaN after every number and equal to every NaN, and
-0.0 equal to 0.0.
Parameters
other— the value to compare with
Returns
- -1, 0 or 1 as this value sorts before, with or after
other
Example
var nan = (0.0 / 0.0) as float;
assert((1.0 as float).compare(nan) < 0, "NaN sorts last");
assert(nan.compare(nan) == 0, "NaNs sort together");
extend double
extend double with Comparable
double values sort by value, with NaN after every number.
Methods
compare
compare(other: double): int
Returns the order of this value and other in a total order: by
value, with NaN after every number and equal to every NaN, and
-0.0 equal to 0.0.
Parameters
other— the value to compare with
Returns
- -1, 0 or 1 as this value sorts before, with or after
other
Example
var nan = 0.0 / 0.0;
assert((1.0).compare(nan) < 0, "NaN sorts last");
assert(nan.compare(nan) == 0, "NaNs sort together");
Ranges
Ranges of numbers: a..b and a..>b.
a..b (half-open) holds a up to but not including b; a..>b
(closed) includes b. Both make a Range<T>, a value that a for loop
steps through without allocating:
for (var i in 0..count) { ... } # 0, 1, ..., count - 1
for (var i in 1..>10) { ... } # 1, 2, ..., 10
Source: runtime/range.pep
Range
[Value] struct Range<T>
A run of consecutive numbers, from start up to end, as a..b and
a..>b make it.
A range is an iterator: next() steps from start up by one, and a
for loop over a range is a counting loop. A range whose start is past
its end yields nothing. A closed range ending at the largest value of its
type (0..>255 as byte) stops there without overflowing.
Ranges are value structs: copying one copies its position, and looping over a range stored in a variable advances that variable.
Type parameters
T— the number type, inferred from the bounds (0..nwith aulongnis aRange<ulong>)
Example
var total = 0;
for (var i in 1..>4) {
total += i; # 1 + 2 + 3 + 4
}
var r = 0..10;
assert(r.contains(9) && !r.contains(10), "half-open");
Fields
start
T start
The first number of the range.
end
T end
The bound the range stops at: the last number when inclusive, else one past it.
inclusive
bool inclusive
Whether end is part of the range: true for a..>b, false for a..b.
Properties
is_empty
var is_empty: bool { get; }
Whether the range holds no numbers, whatever the iteration has reached.
Constructor
Range()
construct(start: T, end: T, inclusive: bool = false)
Makes the range from start to end, as start..end (or
start..>end when inclusive).
Parameters
start— the first numberend— the bound: one past the last number, or the last number wheninclusiveinclusive— whetherendis part of the range
Methods
next
next(): bool
Advances to the next number: start on the first call, then one
more each time.
Once it returns false it keeps returning false.
Returns
- true if there is a current number, false once past the end
current
current(): T
Returns the number the last successful next() moved to.
Returns
- the current number
Panics
- if
next()was never called (“Range.current() before next()”) - if
next()already returned false (“Range.current() past the end”)
contains
contains(value: T): bool
Returns whether value lies in the range, whatever the iteration has
reached.
Parameters
value— the number to test
Returns
- true if
start <= valueandvalueis beforeend(or equal to it, for a closed range)
Array
Source: runtime/array.pep
Array
[__Lazy] struct Array<T>
A growable, contiguous, zero-indexed sequence of T values.
Elements live in one heap buffer that doubles when full (starting at 4
slots), so append is amortized O(1) and indexing is O(1). When T is a
struct type, each slot holds a counted reference, exactly like a
std::vector<std::shared_ptr<T>>: storing an element acquires it,
overwriting or removing one releases the old occupant, and destroying the
Array releases every element it still holds. Moving elements within the
Array (insert, remove_at, swap, reverse) does not change their
reference counts.
Array is itself a struct, so assigning it or passing it to a function
shares the same Array rather than copying its elements; use clone() for
an independent copy.
An empty Array allocates nothing: Array<T>() makes no object until the
Array is first written (append, insert, reserve, …) or shared
(assigned, stored, passed or returned), and reading it (count,
is_empty, capacity, find, contains, iterating) allocates nothing.
Sharing allocates first, so every name for an Array sees the same
elements.
An Array is Iterable<T>: for (var x in a) visits each element in index
order. Every mutating operation (an element store through [], append,
insert, remove_at, pop, clear, a shrinking truncate, swap,
fill, retain, resize, split_off, and reverse, sort or dedup
of two or more elements) invalidates running iterators and slices
(slice), and the next step of such a loop panics with “Array modified
during iteration”.
Type parameters
T— the element type
See also
ArrayIterator
Example
main(): int {
var a = Array<int>();
a.append(1);
a.append(2);
a[0] = 5;
var total = 0;
for (var x in a) {
total = total + x;
}
return total; # 7
}
Properties
count
var count: ulong { get; }
The number of elements in the Array.
capacity
var capacity: ulong { get; }
The number of elements the Array can hold before its buffer must be reallocated.
is_empty
var is_empty: bool { get; }
Whether the Array has no elements.
Constructor
Array()
construct(capacity: ulong = 0)
Creates an empty Array, optionally preallocating room for capacity
elements.
With the default capacity of 0, nothing is allocated, not even the
Array itself, until the Array is first written or shared (see Array).
Preallocating avoids the reallocations append
would otherwise perform while the Array grows to a known size.
Parameters
capacity— the number of element slots to allocate up front;countis still 0
Example
var a = Array<int>(16); # a.count == 0, a.capacity == 16
Methods
iterate
[__Reads] iterate(): ArrayIterator<T>
Returns an iterator over the elements in index order; this is what
for (var x in array) calls (the Iterable<T> protocol).
The iterator holds a reference to the Array, so iterating a temporary
keeps it alive for the whole loop. Modifying the Array while the
iterator is in use makes the iterator’s next next() or current()
panic, except when the Array was empty when the iterator was made:
the iterator then sees no elements and ends.
Returns
- a new iterator positioned before the first element
Example
for (var x in a) {
total = total + x;
}
reserve
reserve(capacity: ulong)
Ensures the buffer has room for at least capacity elements,
reallocating it when it is smaller.
Never shrinks the buffer: a capacity at or below the current one does
nothing. Elements, their order, and their reference counts are
unchanged, and running iterators stay valid. Growing copies the
existing elements, which costs O(count).
Parameters
capacity— the minimum number of element slots required
Panics
- if the buffer must grow while an I/O operation in a task is using it
Example
var a = Array<int>();
a.reserve(8); # a.capacity >= 8
a.reserve(2); # no effect: capacity never shrinks
grow
grow()
Makes room for one more element: when the buffer is full, doubles its capacity (to at least 4); otherwise does nothing.
append and insert call this themselves; callers rarely need it.
Does not change the elements or invalidate iterators.
Panics
- if the buffer must grow while an I/O operation in a task is using it
operator []
[__Reads] [](idx: ulong): T
Returns the element at idx; this is the indexer read a[idx].
Runs in O(1). For a struct T the result is the stored reference
itself; binding it to a variable acquires it like any shared copy.
Parameters
idx— the zero-based position to read
Returns
- the element at
idx
Panics
- if
idx < countdoes not hold (“Array index read out of bounds”)
Example
var first = a[0];
operator []
[](idx: ulong, value: T): T
Replaces the element at idx with value; this is the indexer write
a[idx] = value.
For a struct T the slot acquires value and releases its previous
occupant; assigning an element to its own slot (a[i] = a[i]) leaves
counts unchanged. Only replaces an existing element: use append to
add one. Invalidates running iterators, even when the value is
unchanged.
Parameters
idx— the zero-based position to overwritevalue— the new element
Returns
- the element now stored at
idx(that is,value)
Panics
- if
idx < countdoes not hold (“Array index write out of bounds”)
Example
a[0] = 42;
append
append(item: T)
Adds item to the end of the Array.
Amortized O(1): when the buffer is full it doubles (O(count) for that
call). For a struct T the Array acquires a reference to item.
Invalidates running iterators.
Parameters
item— the element to add
Panics
- if the buffer must grow while an I/O operation in a task is using it
Example
var a = Array<int>();
a.append(1);
a.append(2); # a holds [1, 2]
insert
insert(idx: ulong, item: T)
Inserts item at position idx, shifting the element there and every
later element up by one.
Runs in O(count - idx), plus a reallocation when the buffer is full.
idx == count appends. For a struct T the Array acquires a
reference to item; shifted elements keep their reference counts.
Invalidates running iterators.
Parameters
idx— the positionitemwill occupy; from 0 throughcountitem— the element to insert
Panics
- if
idx <= countdoes not hold (“Array insert index out of bounds”) - if the buffer must grow while an I/O operation in a task is using it
Example
var a = Array<int>();
a.append(1);
a.append(3);
a.insert(1, 2); # a holds [1, 2, 3]
remove_at
remove_at(idx: ulong): T
Removes and returns the element at idx, shifting every later element
down by one.
Runs in O(count - idx). For a struct T the Array’s reference is
transferred to the caller, so the element stays alive as long as the
result is held; shifted elements keep their reference counts. The
capacity is unchanged. Invalidates running iterators.
Parameters
idx— the position of the element to remove
Returns
- the removed element
Panics
- if
idx < countdoes not hold (“Array remove index out of bounds”)
Example
var a = Array<int>();
a.append(1);
a.append(2);
var x = a.remove_at(0); # x == 1; a holds [2]
pop
pop(): T
Removes and returns the last element.
Runs in O(1). For a struct T the Array’s reference is transferred to
the caller. The capacity is unchanged. Invalidates running iterators.
Returns
- the element that was last
Panics
- if the Array is empty (“Array pop on empty Array”)
Example
var top = a.pop();
first
[__Reads] first(): T
Returns the first element without removing it; the same as a[0].
Returns
- the element at index 0
Panics
- if the Array is empty (“Array first on empty Array”)
last
[__Reads] last(): T
Returns the last element without removing it; the same as
a[a.count - 1].
Returns
- the element at index
count - 1
Panics
- if the Array is empty (“Array last on empty Array”)
clear
clear()
Removes every element, leaving the Array empty.
Runs in O(count) for a T holding references, each of which is
released, and in O(1) for a plain value T (int, u8, a tuple of
numbers). The capacity is kept, so refilling the Array does not
reallocate. Always invalidates running iterators, even on an empty
Array.
truncate
truncate(count: ulong)
Shortens the Array to its first count elements, removing the rest.
Runs in O(removed elements). For a struct T each removed element’s
reference is released. A count at or above the current count
does nothing (the Array never grows) and leaves iterators valid; an
actual shrink invalidates them. The capacity is kept.
Parameters
count— the number of leading elements to keep
Example
a.truncate(1); # keeps only a[0]
a.truncate(5); # no effect when a has fewer than 5 elements
swap
swap(a: ulong, b: ulong)
Exchanges the elements at positions a and b.
Runs in O(1). Reference counts are unchanged: the two references just trade slots. Swapping a position with itself is allowed and changes no element, but still invalidates running iterators like every swap.
Parameters
a— the position of the first elementb— the position of the second element
Panics
- if
a < countdoes not hold (“Array swap index out of bounds”) - if
b < countdoes not hold (“Array swap index out of bounds”)
reverse
reverse()
Reverses the order of the elements in place.
Runs in O(count), exchanging elements pairwise, so reference counts are unchanged. An Array of fewer than two elements is left untouched and its iterators stay valid; otherwise running iterators are invalidated.
Example
a.reverse(); # [1, 2, 3] becomes [3, 2, 1]
sort
sort(compare: (T, T) => int)
Sorts the elements in place into the order compare defines. The
sort is stable: elements that compare equal keep their relative
order.
compare(a, b) returns a negative number when a belongs before
b, 0 when their order does not matter, and a positive number when
a belongs after b, the convention String.compare follows. It is
a merge sort: O(count log count) calls to compare, plus a scratch
buffer of count slots. Elements move between the two buffers
without changing any reference count. Invalidates running iterators
when there are two or more elements.
compare must not modify the Array: its elements are spread over
the two buffers while the sort runs.
Parameters
compare— returns the order of its two arguments, as above
Panics
- if
comparemodifies the Array (“Array modified during sort”)
Example
var names = Array<String>();
names.append("pear");
names.append("apple");
names.sort((a, b) => a.compare(b)); # ["apple", "pear"]
var numbers = Array<int>();
numbers.append(3);
numbers.append(1);
numbers.sort((a, b) => b - a); # descending: [3, 1]
find
[__Reads] find(item: T): ulong?
Returns the position of the first element equal to item, or none
when no element matches.
Runs in O(count), scanning from the front. Elements are compared with
==: when T declares an == operator (as String does) that
operator decides; for any other struct T it compares references, so
only the very same object matches.
Breaking change: find used to return a long that was -1 when
nothing matched. It now returns ulong?: test it with is_some() (or
as a condition) and read the index with value().
Parameters
item— the value to look for
Returns
- the index of the first match, or none when no element matches (always none for an empty Array)
Example
var a = [3, 9, 4];
var i = a.find(9); # i.value() == 1
var missing = a.find(7); # missing.is_empty() == true
contains
[__Reads] contains(item: T): bool
Reports whether any element equals item, compared as find does.
Runs in O(count).
Parameters
item— the value to look for
Returns
- true when
find(item)finds a match
slice
[__Reads] slice(start: ulong, end: ulong): ArraySlice<T>
Returns a read-only view of the elements from start up to but not
including end, without copying them.
The slice holds a reference to this Array, so it cannot outlive it,
and it is fail-fast like an iterator: once the Array is modified (see
Array for which operations count), every later use of the slice
panics, so it never reads elements that moved or were freed.
Parameters
start— the position of the first element in the sliceend— the position one past the last element; fromstartthroughcount
Returns
- a view of
end - startelements
Panics
- if
start <= endandend <= countdo not both hold (“Array slice out of bounds”)
Example
var a = [1, 2, 3, 4, 5];
var middle = a.slice(1, 4); # views [2, 3, 4]
var second = middle[1]; # 3
binary_search_by
[__Reads] binary_search_by(order: (T) => int): ulong?
Returns the position of the element that order reports equal to the
target, searching an Array sorted in the order order follows, or
none when there is none.
Runs in O(log count) calls to order, which must not modify the
Array. When several elements match, the first of them is found.
Parameters
order— returns a negative number when its argument sorts before the target, 0 when it matches, and a positive number when it sorts after
Returns
- the index of the first matching element, or none
Panics
- if
ordermodifies the Array (“Array modified while a slice of it is in use”)
Example
var a = [10, 20, 30];
var i = a.binary_search_by((x) => x.compare(20)); # i.value() == 1
fill
fill(value: T)
Replaces every element with value.
Runs in O(count). For a struct T every slot acquires value and
releases its previous occupant. Invalidates running iterators.
Parameters
value— the value each element becomes
Example
var a = [1, 2, 3];
a.fill(0); # [0, 0, 0]
retain
retain(keep: (T) => bool)
Removes every element for which keep returns false, keeping the
order of the rest.
Runs in O(count) calls to keep, which sees each element once, in
index order, and must not modify the Array. For a struct T each
removed element is released and kept elements keep their counts. The
capacity is unchanged. Invalidates running iterators.
Parameters
keep— returns true for an element to keep
Panics
- if
keepmodifies the Array (“Array modified during retain”)
Example
var a = [1, 2, 3, 4];
a.retain((x) => x % 2 == 0); # [2, 4]
dedup
dedup()
Removes each element equal (==) to the one before it, so a run of
equal elements keeps only its first. On a sorted Array this removes
every duplicate.
Runs in O(count). Elements are compared as find does. For a struct
T each removed element is released. Invalidates running iterators
when there are two or more elements.
Example
var a = [1, 1, 2, 3, 3, 3, 1];
a.dedup(); # [1, 2, 3, 1]
resize
resize(count: ulong, value: T)
Changes the count to count: removes elements past it, as
truncate does, or appends copies of value until there are
count elements.
Runs in O(difference in count), plus at most one reallocation. Invalidates running iterators when the count changes.
Parameters
count— the number of elements the Array will holdvalue— the element to append when the Array grows
Panics
- if the buffer must grow while an I/O operation in a task is using it
Example
var a = [1, 2];
a.resize(4, 0); # [1, 2, 0, 0]
a.resize(1, 0); # [1]
split_off
split_off(index: ulong): Array<T>
Removes the elements from index on and returns them as a new Array,
leaving this Array with its first index elements.
Runs in O(count - index). The elements move without changing their reference counts. This Array’s capacity is unchanged. Invalidates running iterators when any element moves.
Parameters
index— the position of the first element to move; from 0 throughcount
Returns
- a new Array holding the elements that were at
indexand after
Panics
- if
index <= countdoes not hold (“Array split_off index out of bounds”)
Example
var a = [1, 2, 3, 4];
var tail = a.split_off(1); # a holds [1]; tail holds [2, 3, 4]
append_all
append_all(rhs: Array<T>)
Appends every element of rhs, in order, to the end of this Array.
Runs in O(count of rhs), growing the buffer at most once and copying
the elements in one block. rhs is unchanged, and it may be this same Array
(a.append_all(a) doubles a). For a struct T each appended element
is acquired once more, since both Arrays now hold it. Invalidates this
Array’s running iterators when rhs is not empty.
Parameters
rhs— the Array whose elements are copied
Panics
- if the buffer must grow while an I/O operation in a task is using it
See also
+=
clone
[__Reads] clone(): Array<T>
Returns a new Array holding the same elements in the same order.
Runs in O(count). The copy is shallow: for a struct T both Arrays
refer to the same element objects (each acquired once more), but adding
or removing elements in one does not affect the other. The new Array’s
capacity equals this Array’s count.
Returns
- an independent Array with the same elements
Example
var b = a.clone();
b.append(7); # a is unchanged
copy
copy(): Array<T>
Returns a deep copy: a new Array holding a copy of each element, made
with copy. This is how copy(value) copies an Array (the
Copyable<Array<T>> implementation); call copy(array) rather than
this method.
Unlike clone(), which shares struct elements, the result shares no
element that is not thread-safe with this Array. An element that
appears twice is copied once. Runs in O(count) plus the cost of
copying the elements.
Returns
- the copy
Example
var names = Array<String>();
names.append("ada");
var copied = copy(names); # calls this method
operator ==
[__Reads] ==(rhs: Array<T>): bool
Reports whether both Arrays have the same count and pairwise equal
elements, compared with == as find does.
Runs in O(count); stops at the first difference. Two empty Arrays are
equal. capacity is not compared.
Parameters
rhs— the Array to compare with
Returns
- true when the Arrays are element-wise equal
operator !=
[__Reads] !=(rhs: Array<T>): bool
Reports whether the Arrays differ in count or in any element; the
negation of ==.
Parameters
rhs— the Array to compare with
Returns
- true when the Arrays are not element-wise equal
operator +=
+=(rhs: Array<T>): Array<T>
Appends every element of rhs to this Array in place (a += b); the
same as append_all.
a += a is allowed and doubles a.
Parameters
rhs— the Array whose elements are appended
Returns
- this Array, after the append
See also
append_all
Example
a += b; # a now ends with b's elements
operator +
[__Reads] +(rhs: Array<T>): Array<T>
Returns a new Array holding this Array’s elements followed by those of
rhs (a + b).
Runs in O(count + count of rhs). Neither operand is modified; like
clone, the result shares struct elements with its operands.
Parameters
rhs— the Array whose elements come second
Returns
- the concatenation of this Array and
rhs
Example
var c = a + b;
extend Array
extend Array<T: Comparable>
The searches of a sorted Array, for an element type with an order
(see Comparable).
Type parameters
T— the element type,Comparable
Methods
binary_search
[__Reads] binary_search(item: T): ulong?
Returns the position of an element equal to item in this Array,
which must be sorted in compare order, or none when there is
none.
Runs in O(log count). When several elements equal item, the first
of them is found. On an Array that is not sorted the result is
unspecified (but never out of bounds).
Parameters
item— the value to look for
Returns
- the index of the first element equal to
item, or none
Example
var a = [1, 3, 5, 7];
var i = a.binary_search(5); # i.value() == 2
lower_bound
[__Reads] lower_bound(item: T): ulong
Returns the first position at which item could be inserted while
keeping this sorted Array sorted: the index of the first element that
does not sort before item (in compare order), or count when
every element does.
Runs in O(log count).
Parameters
item— the value to place
Returns
- the index of the first element at or after
item
Example
var a = [1, 3, 3, 5];
var i = a.lower_bound(3); # 1
upper_bound
[__Reads] upper_bound(item: T): ulong
Returns the last position at which item could be inserted while
keeping this sorted Array sorted: the index of the first element that
sorts after item (in compare order), or count when none does.
Runs in O(log count). upper_bound(x) - lower_bound(x) is the number
of elements equal to x.
Parameters
item— the value to place
Returns
- the index of the first element after
item
Example
var a = [1, 3, 3, 5];
var i = a.upper_bound(3); # 3
is_sorted
[__Reads] is_sorted(): bool
Reports whether the elements are in compare order: no element
sorts after the one that follows it.
Runs in O(count). An Array of fewer than two elements is sorted.
Returns
- true when the Array is sorted
ArrayIterator
[Value] struct ArrayIterator<T>
An Iterator<T> over the elements of an Array, in index order; returned by
Array.iterate() and driven by for (var x in array).
Holds a reference to the Array, so iterating a temporary keeps it alive for
the whole loop. The iterator is fail-fast: if the Array is modified after
the iterator was created (see Array for which operations count), its next
next() or current() panics instead of skipping or repeating elements
(an iterator over an Array that was empty when it was made just ends).
Type parameters
T— the element type of the source Array
See also
Array.iterate
Example
var it = a.iterate();
while (it.next()) {
var x = it.current();
}
Methods
next
next(): bool
Advances to the next element.
The first call moves to index 0. Once it returns false it keeps returning false.
Returns
- true when there is a current element, false once past the end
Panics
- if the Array was modified since the iterator was created (“Array modified during iteration”)
current
current(): T
Returns the element the last successful next() moved to.
Returns
- the current element
Panics
- if the Array was modified since the iterator was created (“Array modified during iteration”)
- if
next()was never called (“ArrayIterator.current() before next()”) - if
next()already returned false (“ArrayIterator.current() past the end”)
ArraySlice
[Value] struct ArraySlice<T>
A read-only view of a run of an Array’s elements, made by Array.slice,
that lets a function take “some of the elements” without copying them.
A slice holds a reference to its Array, so the Array lives at least as
long as the slice. It is fail-fast like ArrayIterator: once the Array
is modified (see Array for which operations count), every later use of
the slice panics with “Array modified while a slice of it is in use”
instead of reading elements that moved or were freed. Make a new slice
after changing the Array.
A slice is Iterable<T>: for (var x in slice) visits its elements in
order.
Type parameters
T— the element type of the Array
See also
Array.slice
Example
total(values: ArraySlice<int>): int {
var sum = 0;
for (var x in values) {
sum = sum + x;
}
return sum;
}
var a = [1, 2, 3, 4];
var tail_sum = total(a.slice(2, 4)); # 7
Properties
count
var count: ulong { get; }
The number of elements in the slice.
is_empty
var is_empty: bool { get; }
Whether the slice has no elements.
Methods
operator []
[](index: ulong): T
Returns the element at index in the slice, index 0 being the
slice’s first element; this is the indexer read slice[index].
Parameters
index— the zero-based position in the slice
Returns
- the element at
index
Panics
- if the Array was modified since the slice was made (“Array modified while a slice of it is in use”)
- if
index < countdoes not hold (“ArraySlice index out of bounds”)
slice
slice(start: ulong, end: ulong): ArraySlice<T>
Returns a view of this slice’s elements from start up to but not
including end, positions counted within this slice.
Parameters
start— the position of the first elementend— the position one past the last element; fromstartthroughcount
Returns
- a view of
end - startelements of the same Array
Panics
- if the Array was modified since the slice was made (“Array modified while a slice of it is in use”)
- if
start <= endandend <= countdo not both hold (“ArraySlice slice out of bounds”)
iterate
iterate(): ArraySliceIterator<T>
Returns an iterator over the slice’s elements in order; this is what
for (var x in slice) calls.
Returns
- a new iterator positioned before the first element
to_array
to_array(): Array<T>
Returns a new Array holding a copy of the slice’s elements, in order.
The copy is shallow, as Array.clone is: for a struct T the new
Array shares the element objects.
Returns
- an independent Array with the slice’s elements
Panics
- if the Array was modified since the slice was made (“Array modified while a slice of it is in use”)
find
find(item: T): ulong?
Returns the position in the slice of the first element equal (==)
to item, or none, compared as Array.find does.
Parameters
item— the value to look for
Returns
- the index of the first match, or none when no element matches
Panics
- if the Array was modified since the slice was made (“Array modified while a slice of it is in use”)
contains
contains(item: T): bool
Reports whether any element equals item, compared as find does.
Parameters
item— the value to look for
Returns
- true when
find(item)finds a match
Panics
- if the Array was modified since the slice was made (“Array modified while a slice of it is in use”)
binary_search_by
binary_search_by(order: (T) => int): ulong?
Returns the position of the first element that order reports equal
to the target, in a slice sorted in the order order follows, or
none; see Array.binary_search_by.
Parameters
order— returns a negative number when its argument sorts before the target, 0 when it matches, and a positive number when it sorts after
Returns
- the index of the first matching element, or none
Panics
- if the Array was modified since the slice was made,
orderincluded (“Array modified while a slice of it is in use”)
extend ArraySlice
extend ArraySlice<T: Comparable>
The searches of a sorted ArraySlice, for an element type with an order
(see Comparable).
Type parameters
T— the element type,Comparable
Methods
binary_search
binary_search(item: T): ulong?
Returns the position of the first element equal to item in this
slice, which must be sorted in compare order, or none; see
Array.binary_search.
Parameters
item— the value to look for
Returns
- the index of the first element equal to
item, or none
Panics
- if the Array was modified since the slice was made (“Array modified while a slice of it is in use”)
lower_bound
lower_bound(item: T): ulong
Returns the index of the first element of this sorted slice that
does not sort before item, or count; see Array.lower_bound.
Parameters
item— the value to place
Returns
- the index of the first element at or after
item
Panics
- if the Array was modified since the slice was made (“Array modified while a slice of it is in use”)
upper_bound
upper_bound(item: T): ulong
Returns the index of the first element of this sorted slice that
sorts after item, or count; see Array.upper_bound.
Parameters
item— the value to place
Returns
- the index of the first element after
item
Panics
- if the Array was modified since the slice was made (“Array modified while a slice of it is in use”)
is_sorted
is_sorted(): bool
Reports whether the elements are in compare order; see
Array.is_sorted.
Returns
- true when no element sorts after the one that follows it
Panics
- if the Array was modified since the slice was made (“Array modified while a slice of it is in use”)
ArraySliceIterator
[Value] struct ArraySliceIterator<T>
An Iterator<T> over the elements of an ArraySlice, in order; returned
by ArraySlice.iterate() and driven by for (var x in slice).
Type parameters
T— the element type of the slice
See also
ArraySlice.iterate
Methods
next
next(): bool
Advances to the next element.
Returns
- true when there is a current element, false once past the end
Panics
- if the Array was modified since the slice was made (“Array modified while a slice of it is in use”)
current
current(): T
Returns the element the last successful next() moved to.
Returns
- the current element
Panics
- if the Array was modified since the slice was made (“Array modified while a slice of it is in use”)
- if
next()was never called (“ArraySliceIterator.current() before next()”) - if
next()already returned false (“ArraySlice index out of bounds”)
HashMap and HashSet
Hashing: hash<T>(value), the 64-bit hash HashMap and HashSet use for
their keys, and the hash maps and sets themselves.
Source: runtime/hash.pep
hash
hash<T>(value: T): u64
Returns a 64-bit hash of value, the hash HashMap and HashSet use for
their keys.
- POD types (integers, floats, pointers,
bool) mix the value’s own bits, so equal values hash equally. - Struct types hash by identity: the object’s address, mixed so that aligned addresses spread across buckets. Two distinct objects with equal fields therefore hash differently and are distinct keys. A null reference hashes to the FNV-1a offset basis (0xcbf29ce484222325).
Every input bit reaches the low bits of the result, which a power-of-two table masks with. The hash is not keyed, so it does not resist inputs chosen to collide.
Stringhashes by content through the concretehash<String>override, so equal strings at different addresses collide.
A struct whose keys should match by content is marked [Equatable],
which derives its == and its hash from its fields, or declares an ==
operator and overrides this function for itself, hashing the fields ==
compares: hash<Point>(value: Point): u64. Keys that are == must hash
equally (see the “Hashing and equality” chapter of the book).
The result is deterministic within and across runs of a program for POD values and strings; for other struct types it depends on where the object was allocated. Reading the address does no refcount work, and a temporary argument is released afterwards.
Type parameters
T— the type of the value to hash
Parameters
value— the value to hash
Returns
- the 64-bit hash of
value
See also
hash<String>
Example
var a = hash<int>(5);
var b = hash<int>(5); # a == b
hash
hash<String>(value: String): u64
Returns a 64-bit hash of a String’s bytes, so equal strings hash equally
wherever they are stored. Keeps equal String keys of a HashMap or
HashSet in one bucket.
Hashes the size bytes of the String (not the NUL terminator) 8 bytes
at a time, in O(size). An empty String hashes to the FNV-1a offset basis
(0xcbf29ce484222325).
Parameters
value— the String to hash; it must not be a null reference
Returns
- the 64-bit hash of the String’s content
Example
assert(hash<String>("hello") == hash<String>("hello"), "content hash");
hash
hash<StringConstant>(value: StringConstant): u64
Returns a 64-bit hash of a StringConstant’s bytes, the same hash
hash<String> gives a String with the same content, so StringConstant
keys work in a HashMap or HashSet. Runs in O(size).
Parameters
value— the text to hash
Returns
- the 64-bit hash of the text’s content
HashMap
struct HashMap<K, V>
A hash table that maps keys of type K to values of type V.
Keys are hashed with hash<K> and compared with ==: POD keys by value,
String keys by content, and other struct keys by identity (two distinct
objects with equal fields are different keys) unless the struct declares
an == operator and a matching hash<K> override. Any type can be a key.
The map holds one reference to each stored key and value. It acquires them when they are stored and releases them when they are overwritten, removed, cleared, or when the map is destroyed.
Lookups, inserts, and removals take expected O(1) time. Before an insert of a new key would fill half the table (live entries plus removed-slot tombstones), the table is rebuilt: at double the size, or at the same size when tombstones make up at least half of that load, so churn (many inserts and removals of different keys) does not keep growing it. It never shrinks.
iterate() visits the entries as (key, value) tuples, in no defined order.
Type parameters
K— the key type; hashed withhash<K>and compared with==V— the value type
Example
var ages = HashMap<String, int>();
ages["ada"] = 36;
ages.insert("alan", 41);
assert(ages["ada"] == 36, "lookup");
assert(ages.get("grace") == 0, "a missing key reads as the default");
assert(ages.remove("alan"), "removed");
assert(ages.count == 1, "one entry left");
Properties
count
var count: int { get; }
The number of entries in the map. O(1).
is_empty
var is_empty: bool { get; }
Whether the map has no entries. O(1).
capacity
var capacity: ulong { get; }
The number of slots in the table, a power of two and at least 8. The map grows before its entries and tombstones reach half of it. O(1).
Constructor
HashMap()
construct(initial_capacity: ulong = 8)
Creates an empty map whose table has room for at least
initial_capacity slots.
The slot count is rounded up to a power of two, with a minimum of 8. The map holds up to half its slot count before the first grow, so pass about twice the expected number of entries to avoid regrowing. Allocates three arrays of that many slots.
Parameters
initial_capacity— the minimum number of slots in the table
Example
var m = HashMap<int, int>(64); # 64 slots, 32 entries before growing
Methods
grow
grow()
Doubles the table and reinserts every entry, dropping all tombstones.
insert calls it automatically. Calling it directly is safe but only
costs memory. Runs in O(capacity). It replaces the table’s arrays and
moves entries to new slots. Reference counts are unchanged overall:
each entry is acquired by the new arrays and released by the old ones.
cap
[Deprecated("use the 'capacity' property instead")] cap(): ulong
Deprecated: use capacity instead. Returns the number of slots in
the table.
Returns
- the same as
capacity
clear
clear()
Removes every entry and releases the map’s references to their keys and values. The table keeps its capacity. Runs in O(capacity).
copy
copy(): HashMap<K, V>
Returns a deep copy: a new map holding a copy of each key and value,
made with copy. This is how copy(value) copies a HashMap
(the Copyable<HashMap<K, V>> implementation); call copy(map)
rather than this method.
The copied keys are hashed again, so a map keyed by struct identity finds its entries under the copied keys. Runs in O(capacity) plus the cost of copying the entries.
Returns
- the copy
contains
contains(key: K): bool
Returns whether the map has an entry for key. Expected O(1).
Parameters
key— the key to look up
Returns
- true when
keyis present
insert
insert(key: K, value: V): bool
Maps key to value, adding a new entry or overwriting the value of
an existing one.
When the key is already present, only the value is replaced: the map
keeps the key object it already stores and releases the old value.
A new entry acquires both key and value, and may first grow the
table (see grow). Expected O(1); a grow costs O(capacity).
Parameters
key— the key to add or updatevalue— the value to store forkey
Returns
- true when a new key was added, false when an existing key’s value was overwritten
Example
var m = HashMap<int, int>();
m.insert(1, 10); # true: added
m.insert(1, 20); # false: overwritten; m.get(1) == 20
get
get(key: K): V
Returns the value stored for key, or the default of V when key
is absent.
The default is 0 for numbers and a null reference for struct types,
including String. A missing key is not an error and is not
inserted. When V is a struct type, call contains first rather than
using a null result. The caller gets its own reference to the stored
value. Expected O(1).
Parameters
key— the key to look up
Returns
- the value for
key, or the default ofVwhen absent
try_get
try_get(key: K): V?
Returns the value stored for key, or none when key is absent.
Expected O(1).
Parameters
key— the key to look up
Returns
- the value for
key, or none when absent
Example
if (ages.try_get("ada") is Value(age)) {
print($"{age}");
}
remove
remove(key: K): bool
Removes the entry for key, releasing the map’s references to its key
and value. Does nothing when key is absent.
The slot becomes a tombstone: occupied for probing, empty for lookup. It counts toward the grow threshold until an insert reuses it or the table grows. Expected O(1).
Parameters
key— the key to remove
Returns
- true when
keywas present and removed, false when absent
operator []
[](key: K): V
Reads m[key]: returns the value for key, or the default of V
(0, or a null reference for struct types) when key is absent. The
same as get. A missing key is not inserted.
Parameters
key— the key to look up
Returns
- the value for
key, or the default ofVwhen absent
Example
var m = HashMap<String, int>();
var n = m["missing"]; # 0; m is still empty
operator []
[](key: K, value: V): V
Writes m[key] = value: inserts key or overwrites its value, the
same as insert.
Parameters
key— the key to add or updatevalue— the value to store forkey
Returns
value, the value of the assignment expression
Example
var m = HashMap<String, int>();
m["hello"] = 5;
iterate
iterate(): HashMapIterator<K, V>
Returns an iterator over the entries as (key, value) tuples, in no
defined order; for (var (key, value) in map) drives it.
The iterator holds a reference to the map. Changing a value through an
existing key while iterating is allowed; adding or removing an entry
makes the iterator’s next next() or current() panic.
Returns
- an iterator positioned before the first entry
Example
var total = 0;
for (var (name, age) in ages) {
total += age;
}
HashSet
struct HashSet<K>
A hash set of distinct elements of type K, built on the same table as
HashMap but without values.
Elements are hashed with hash<K> and compared with ==: POD elements
by value, String elements by content, and other struct elements by
identity (two distinct objects with equal fields are both members)
unless the struct declares an == operator and a matching hash<K>
override. The
set holds one reference to each member. It acquires the reference on
insert and releases it on remove, on clear, or when the set is
destroyed.
Membership tests, inserts, and removals take expected O(1) time. Before
an insert of a new element would fill half the table (members plus
tombstones), the table is rebuilt: at double the size, or at the same
size when tombstones make up at least half of that load. It never
shrinks. iterate() visits the
members, in no defined order. The lowercase fields and the _find_slot
and grow methods are the table’s internal state: they are public but
may change, and writing to them breaks the set.
Type parameters
K— the element type; hashed withhash<K>and compared with==
Example
var seen = HashSet<String>();
seen.insert("a");
seen["b"] = "b"; # the same as seen.insert("b")
assert(seen.contains("a"), "member");
assert(!seen.insert("a"), "already a member");
assert(seen.count == 2, "two members");
Properties
count
var count: int { get; }
The number of members in the set. O(1).
is_empty
var is_empty: bool { get; }
Whether the set has no members. O(1).
capacity
var capacity: ulong { get; }
The number of slots in the table, a power of two and at least 8. O(1).
Constructors
HashSet()
construct(initial_capacity: ulong = 8)
Creates an empty set whose table has room for at least
initial_capacity slots.
The slot count is rounded up to a power of two, with a minimum of 8. The set holds up to half its slot count before the first grow.
Parameters
initial_capacity— the minimum number of slots in the table
HashSet()
[Implicit] construct(items: Array<K>)
Creates a set holding each element of items once.
The constructor is [Implicit]: an array converts to a set wherever
a set is expected, so an array literal can initialize one.
Parameters
items— the elements to insert; duplicates are kept once
Example
HashSet<String> keywords = ["if", "else", "while"];
assert(keywords.contains("else"), "a member");
Methods
grow
grow()
Doubles the table and reinserts every member, dropping all tombstones.
insert calls it automatically. Calling it directly is safe but only
costs memory. Runs in O(capacity). It replaces the table’s arrays and
moves members to new slots.
cap
[Deprecated("use the 'capacity' property instead")] cap(): ulong
Deprecated: use capacity instead. Returns the number of slots in
the table.
Returns
- the same as
capacity
clear
clear()
Removes every member and releases the set’s references to them. The table keeps its capacity. Runs in O(capacity).
contains
contains(key: K): bool
Returns whether key is a member of the set. Expected O(1).
Parameters
key— the element to look up
Returns
- true when
keyis a member
insert
insert(key: K): bool
Adds key to the set if it is not already a member.
A new member is acquired, and the insert may first grow the table
(see grow). When an equal member already exists, the set keeps the
member it has and does not store key. Expected O(1); a grow costs
O(capacity).
Parameters
key— the element to add
Returns
- true when
keywas added, false when it was already a member
remove
remove(key: K): bool
Removes key from the set, releasing the set’s reference to the
member. Does nothing when key is not a member. The slot becomes a
tombstone (see _tombstones). Expected O(1).
Parameters
key— the element to remove
Returns
- true when
keywas a member and was removed, false otherwise
operator []
[](key: K): K
Reads s[key]: returns the stored member equal to key, or the
default of K (0, or a null reference for struct types) when key is
not a member.
For String elements this returns the instance the set stores, which
may be a different object than key with the same content. A missing
element is not inserted.
Parameters
key— the element to look up
Returns
- the stored member equal to
key, or the default ofK
operator []
[](key: K, value: K): K
Writes s[key] = value: makes key a member, the same as
insert(key).
A set stores only keys, so value is ignored.
Parameters
key— the element to addvalue— ignored
Returns
key(the argument, not an existing equal member), the value of the assignment expression
iterate
iterate(): HashSetIterator<K>
Returns an iterator over the members, in no defined order;
for (var x in set) drives it.
The iterator holds a reference to the set. Adding or removing a member
while iterating makes the iterator’s next next() or current() panic.
Returns
- an iterator positioned before the first member
Example
for (var name in seen) {
print(name);
}
copy
copy(): HashSet<K>
Returns a deep copy: a new set holding a copy of each element, made
with copy. This is how copy(value) copies a HashSet (the
Copyable<HashSet<K>> implementation); call copy(set) rather than
this method.
The copied elements are hashed again, so a set of structs compared by identity finds the copied elements. Runs in O(capacity) plus the cost of copying the elements.
Returns
- the copy
HashMapIterator
[Value] struct HashMapIterator<K, V>
An Iterator<(K, V)> over the entries of a HashMap, as (key, value)
tuples in no defined order; returned by HashMap.iterate() and driven by
for (var (key, value) in map).
Holds a reference to the map. It is fail-fast: adding or removing an
entry after the iterator was created makes its next next() or
current() panic.
Type parameters
K— the key type of the mapV— the value type of the map
See also
HashMap.iterate
Constructor
HashMapIterator()
construct(source: HashMap<K, V>)
Creates an iterator positioned before the first entry of source.
Parameters
source— the map to iterate
Methods
next
next(): bool
Advances to the next entry.
Returns
- true when there is a current entry, false once every entry was visited
Panics
- if an entry was added or removed since the iterator was created (“HashMap modified during iteration”)
current
current(): (K, V)
Returns the entry the last successful next() moved to.
Returns
- the current entry as a
(key, value)tuple
Panics
- if an entry was added or removed since the iterator was created (“HashMap modified during iteration”)
- if
next()was never called or already returned false (“HashMapIterator.current() without an entry”)
HashSetIterator
[Value] struct HashSetIterator<K>
An Iterator<K> over the members of a HashSet, in no defined order;
returned by HashSet.iterate() and driven by for (var x in set).
Holds a reference to the set. It is fail-fast: adding or removing a member
after the iterator was created makes its next next() or current()
panic.
Type parameters
K— the element type of the set
See also
HashSet.iterate
Constructor
HashSetIterator()
construct(source: HashSet<K>)
Creates an iterator positioned before the first member of source.
Parameters
source— the set to iterate
Methods
next
next(): bool
Advances to the next member.
Returns
- true when there is a current member, false once every member was visited
Panics
- if a member was added or removed since the iterator was created (“HashSet modified during iteration”)
current
current(): K
Returns the member the last successful next() moved to.
Returns
- the current member
Panics
- if a member was added or removed since the iterator was created (“HashSet modified during iteration”)
- if
next()was never called or already returned false (“HashSetIterator.current() without a member”)
Map and Set
Sorted collections: Map<K, V> and Set<K>, kept in key order on a
red-black tree, and compare<T>, the ordering they sort keys by.
Source: runtime/map.pep
compare
compare<T>(a: T, b: T): int
Returns a negative number when a sorts before b, zero when they are
equal, and a positive number when a sorts after b: the order Map and
Set keep their keys in.
The generic version compares with <, so it works for every
Comparable type (see Comparable), every integer type, char, and any
struct that declares a < operator. Two values neither of which is <
the other are equal keys. String, float and double go through
concrete overrides that call their compare, so a NaN key sorts after
every number instead of breaking the order.
A struct with no < operator declares Comparable, or overrides this
function for itself, to be a Map key or Set element:
compare<Point>(a: Point, b: Point): int. The order must be a strict
weak order (consistent across calls), or the tree loses track of its
keys.
Type parameters
T— the type of the values to compare
Parameters
a— the first valueb— the second value
Returns
- a negative number, zero or a positive number as
asorts before, equal to or afterb
See also
compare<String>
Example
assert(compare<int>(1, 2) < 0, "1 sorts first");
assert(compare<int>(2, 2) == 0, "equal");
compare
compare<String>(a: String, b: String): int
Compares two Strings by content, the order Map and Set keep String
keys in: the same as a.compare(b).
Parameters
a— the first Stringb— the second String
Returns
- a negative number, zero or a positive number as
asorts before, equal to or afterb
Example
assert(compare<String>("apple", "pear") < 0, "apple sorts first");
compare
compare<float>(a: float, b: float): int
Compares two floats in float.compare’s total order: by value, with
NaN after every number.
Parameters
a— the first valueb— the second value
Returns
- -1, 0 or 1 as
asorts before, with or afterb
compare
compare<double>(a: double, b: double): int
Compares two doubles in double.compare’s total order: by value, with
NaN after every number.
Parameters
a— the first valueb— the second value
Returns
- -1, 0 or 1 as
asorts before, with or afterb
Map
struct Map<K, V>
A map from keys of type K to values of type V that keeps its keys
sorted.
Keys are ordered by compare<K>: numbers by value, String by content,
and other structs by their < operator or a compare<K> override. Two
keys that compare equal are the same key. Lookups, inserts and removals
take O(log count) time on a red-black tree.
iterate() visits the entries as (key, value) tuples in ascending key
order, reversed() in descending order, and range(from, to) the keys
from from up to (not including) to. first, last, lower_bound
and upper_bound find single entries by order.
The map holds one reference to each stored key and value. It acquires them when they are stored and releases them when they are overwritten, removed, cleared, or when the map is destroyed.
For keys kept in the order they were inserted, use OrderedMap; for
expected O(1) lookups in no order, HashMap.
Type parameters
K— the key type; ordered bycompare<K>V— the value type
Example
var scores = Map<String, int>();
scores["carol"] = 91;
scores["alice"] = 84;
scores["bob"] = 77;
for (var (name, score) in scores) {
print($"{name}: {score}"); # alice, bob, carol
}
Properties
count
var count: int { get; }
The number of entries in the map. O(1).
is_empty
var is_empty: bool { get; }
Whether the map has no entries. O(1).
Constructor
Map()
construct()
Creates an empty map.
Methods
clear
clear()
Removes every entry and releases the map’s references to their keys and values. O(count).
contains
contains(key: K): bool
Returns whether the map has an entry for key. O(log count).
Parameters
key— the key to look up
Returns
- true when
keyis present
insert
insert(key: K, value: V): bool
Maps key to value, adding a new entry or overwriting the value of
an existing one.
When the key is already present, only the value is replaced: the map keeps the key object it already stores and releases the old value. O(log count).
Parameters
key— the key to add or updatevalue— the value to store forkey
Returns
- true when a new key was added, false when an existing key’s value was overwritten
get
get(key: K): V
Returns the value stored for key, or the default of V (0, or a
null reference for struct types) when key is absent. A missing key
is not inserted. O(log count).
Parameters
key— the key to look up
Returns
- the value for
key, or the default ofVwhen absent
See also
try_get
try_get
try_get(key: K): V?
Returns the value stored for key, or none when key is absent.
O(log count).
Parameters
key— the key to look up
Returns
- the value for
key, or none when absent
Example
if (scores.try_get("alice") is Value(score)) {
print($"{score}");
}
remove
remove(key: K): bool
Removes the entry for key, releasing the map’s references to its
key and value. Does nothing when key is absent. O(log count).
Parameters
key— the key to remove
Returns
- true when
keywas present and removed, false when absent
operator []
[](key: K): V
Reads m[key]: the same as get. A missing key reads as the default
of V and is not inserted.
Parameters
key— the key to look up
Returns
- the value for
key, or the default ofVwhen absent
operator []
[](key: K, value: V): V
Writes m[key] = value: the same as insert.
Parameters
key— the key to add or updatevalue— the value to store forkey
Returns
value, the value of the assignment expression
first
first(): (K, V)?
Returns the entry with the smallest key. O(log count).
Returns
- the first
(key, value)in key order, or none when the map is empty
last
last(): (K, V)?
Returns the entry with the largest key. O(log count).
Returns
- the last
(key, value)in key order, or none when the map is empty
lower_bound
lower_bound(key: K): (K, V)?
Returns the first entry whose key is not less than key: the entry
for key itself when present, else the next one after it. O(log
count).
Parameters
key— the key to search from
Returns
- the first
(key, value)with a key>= key, or none when every key is less
Example
var after_b = scores.lower_bound("b"); # ("bob", 77)
upper_bound
upper_bound(key: K): (K, V)?
Returns the first entry whose key is greater than key. O(log count).
Parameters
key— the key to search from
Returns
- the first
(key, value)with a key> key, or none when no key is greater
iterate
iterate(): SortedMapIterator<K, V>
Returns an iterator over the entries as (key, value) tuples in
ascending key order; for (var (key, value) in map) drives it.
The iterator holds a reference to the map. Changing a value through
an existing key while iterating is allowed; adding or removing an
entry makes the iterator’s next next() or current() panic.
Returns
- an iterator positioned before the first entry
reversed
reversed(): SortedMapIterator<K, V>
Returns an iterator over the entries in descending key order, under
the same rules as iterate.
Returns
- an iterator positioned before the last entry
Example
for (var (name, score) in scores.reversed()) {
print(name); # carol, bob, alice
}
range
range(from: K, to: K): SortedMapIterator<K, V>
Returns an iterator over the entries whose keys are at least from
and less than to, in ascending key order, under the same rules as
iterate. It is empty when from is not less than to.
Parameters
from— the smallest key to visitto— the key to stop before
Returns
- an iterator positioned before the first entry in the range
Example
for (var (name, score) in scores.range("a", "c")) {
print(name); # alice, bob
}
copy
copy(): Map<K, V>
Returns a deep copy: a new map holding a copy of each key and value,
made with copy. This is how copy(value) copies a Map (the
Copyable<Map<K, V>> implementation); call copy(map) rather than
this method. O(count log count) plus the cost of copying the entries.
Returns
- the copy
SortedMapIterator
[Value] struct SortedMapIterator<K, V>
An iterator over the entries of a Map as (key, value) tuples, in key
order or reversed; returned by Map.iterate(), reversed() and
range(from, to), and driven by for (var (key, value) in ...).
Holds a reference to the map. It is fail-fast: adding or removing an
entry after the iterator was created makes its next next() or
current() panic.
Type parameters
K— the key type of the mapV— the value type of the map
See also
Map.iterate
Constructor
SortedMapIterator()
construct(source: Map<K, V>, start: ulong, stop: ulong, reverse: bool)
Creates an iterator over source from node start up to node stop.
Internal: call Map.iterate, reversed or range instead.
Parameters
source— the map to iteratestart— the first node to visitstop— the node to stop before, 0 for the endreverse— whether to step in descending key order
Methods
next
next(): bool
Advances to the next entry.
Returns
- true when there is a current entry, false once every entry was visited
Panics
- if an entry was added or removed since the iterator was created (“Map modified during iteration”)
current
current(): (K, V)
Returns the entry the last successful next() moved to.
Returns
- the current entry as a
(key, value)tuple
Panics
- if an entry was added or removed since the iterator was created (“Map modified during iteration”)
- if
next()was never called or already returned false (“SortedMapIterator.current() without an entry”)
Set
struct Set<K>
A set of distinct elements of type K kept in sorted order, on the same
red-black tree as Map but without values.
Elements are ordered by compare<K> (see Map); two elements that
compare equal are the same member. Membership tests, inserts and removals
take O(log count) time. iterate() visits the members in ascending
order, reversed() in descending order, and range(from, to) the
members from from up to (not including) to.
The set holds one reference to each member, acquired on insert and
released on remove, on clear, or when the set is destroyed.
Type parameters
K— the element type; ordered bycompare<K>
Example
Set<int> primes = [7, 2, 5, 3];
for (var p in primes) {
print($"{p}"); # 2, 3, 5, 7
}
Properties
count
var count: int { get; }
The number of members. O(1).
is_empty
var is_empty: bool { get; }
Whether the set has no members. O(1).
Constructors
Set()
construct()
Creates an empty set.
Set()
[Implicit] construct(items: Array<K>)
Creates a set holding each element of items once.
The constructor is [Implicit]: an array converts to a set wherever
a set is expected, so an array literal can initialize one.
Parameters
items— the elements to insert; duplicates are kept once
Example
Set<String> keywords = ["while", "if", "else"];
assert(keywords.first().value() == "else", "sorted");
Methods
clear
clear()
Removes every member and releases the set’s references to them. O(count).
contains
contains(key: K): bool
Returns whether key is a member. O(log count).
Parameters
key— the element to look up
Returns
- true when
keyis a member
insert
insert(key: K): bool
Adds key to the set if it is not already a member. When an equal
member exists, the set keeps the member it has. O(log count).
Parameters
key— the element to add
Returns
- true when
keywas added, false when it was already a member
remove
remove(key: K): bool
Removes key from the set, releasing the set’s reference to the
member. Does nothing when key is not a member. O(log count).
Parameters
key— the element to remove
Returns
- true when
keywas a member and was removed, false otherwise
first
first(): K?
Returns the smallest member. O(log count).
Returns
- the smallest member, or none when the set is empty
last
last(): K?
Returns the largest member. O(log count).
Returns
- the largest member, or none when the set is empty
lower_bound
lower_bound(key: K): K?
Returns the smallest member that is not less than key. O(log count).
Parameters
key— the element to search from
Returns
- the first member
>= key, or none when every member is less
upper_bound
upper_bound(key: K): K?
Returns the smallest member greater than key. O(log count).
Parameters
key— the element to search from
Returns
- the first member
> key, or none when no member is greater
iterate
iterate(): SortedSetIterator<K>
Returns an iterator over the members in ascending order;
for (var x in set) drives it.
The iterator holds a reference to the set. Adding or removing a member
while iterating makes the iterator’s next next() or current()
panic.
Returns
- an iterator positioned before the smallest member
reversed
reversed(): SortedSetIterator<K>
Returns an iterator over the members in descending order, under the
same rules as iterate.
Returns
- an iterator positioned before the largest member
range
range(from: K, to: K): SortedSetIterator<K>
Returns an iterator over the members that are at least from and
less than to, in ascending order, under the same rules as iterate.
It is empty when from is not less than to.
Parameters
from— the smallest member to visitto— the element to stop before
Returns
- an iterator positioned before the first member in the range
copy
copy(): Set<K>
Returns a deep copy: a new set holding a copy of each member, made
with copy. This is how copy(value) copies a Set (the
Copyable<Set<K>> implementation); call copy(set) rather than this
method.
Returns
- the copy
SortedSetIterator
[Value] struct SortedSetIterator<K>
An Iterator<K> over the members of a Set, in order or reversed;
returned by Set.iterate(), reversed() and range(from, to), and
driven by for (var x in ...).
Holds a reference to the set. It is fail-fast: adding or removing a member
after the iterator was created makes its next next() or current()
panic.
Type parameters
K— the element type of the set
See also
Set.iterate
Constructor
SortedSetIterator()
construct(source: Set<K>, start: ulong, stop: ulong, reverse: bool)
Creates an iterator over source from node start up to node stop.
Internal: call Set.iterate, reversed or range instead.
Parameters
source— the set to iteratestart— the first node to visitstop— the node to stop before, 0 for the endreverse— whether to step in descending order
Methods
next
next(): bool
Advances to the next member.
Returns
- true when there is a current member, false once every member was visited
Panics
- if a member was added or removed since the iterator was created (“Set modified during iteration”)
current
current(): K
Returns the member the last successful next() moved to.
Returns
- the current member
Panics
- if a member was added or removed since the iterator was created (“Set modified during iteration”)
- if
next()was never called or already returned false (“SortedSetIterator.current() without a member”)
OrderedMap
OrderedMap<K, V>: a hash map that keeps its keys in the order they were
first inserted.
Source: runtime/ordered_map.pep
OrderedMap
struct OrderedMap<K, V>
A hash map that iterates its entries in the order their keys were first
inserted, like Python’s dict.
Keys are hashed with hash<K> and compared with ==, as in HashMap.
Lookups, inserts and removals take expected O(1) time. Setting the value
of a key that is already present keeps its position; removing a key and
inserting it again moves it to the end.
iterate() visits the entries as (key, value) tuples in insertion
order. The map holds one reference to each stored key and value, acquired
when they are stored and released when they are overwritten, removed,
cleared, or when the map is destroyed.
Type parameters
K— the key type; hashed withhash<K>and compared with==V— the value type
Example
var config = OrderedMap<String, String>();
config["name"] = "pepper";
config["version"] = "0.2";
config["license"] = "MIT";
for (var (key, value) in config) {
print($"{key} = {value}"); # name, version, license
}
Properties
count
var count: int { get; }
The number of entries in the map. O(1).
is_empty
var is_empty: bool { get; }
Whether the map has no entries. O(1).
Constructor
OrderedMap()
construct(initial_capacity: ulong = 8)
Creates an empty map whose table has room for at least
initial_capacity slots (see HashMap).
Parameters
initial_capacity— the minimum number of slots in the table
Methods
clear
clear()
Removes every entry and releases the map’s references to their keys and values. O(capacity).
contains
contains(key: K): bool
Returns whether the map has an entry for key. Expected O(1).
Parameters
key— the key to look up
Returns
- true when
keyis present
insert
insert(key: K, value: V): bool
Maps key to value. A new key is added at the end of the order; an
existing key keeps its position (and the key object the map already
stores), and only its value is replaced. Expected O(1).
Parameters
key— the key to add or updatevalue— the value to store forkey
Returns
- true when a new key was added, false when an existing key’s value was overwritten
get
get(key: K): V
Returns the value stored for key, or the default of V (0, or a
null reference for struct types) when key is absent. A missing key
is not inserted. Expected O(1).
Parameters
key— the key to look up
Returns
- the value for
key, or the default ofVwhen absent
See also
try_get
try_get
try_get(key: K): V?
Returns the value stored for key, or none when key is absent.
Expected O(1).
Parameters
key— the key to look up
Returns
- the value for
key, or none when absent
remove
remove(key: K): bool
Removes the entry for key, releasing the map’s references to its
key and value. The other entries keep their order. Does nothing when
key is absent. Amortized expected O(1).
Parameters
key— the key to remove
Returns
- true when
keywas present and removed, false when absent
operator []
[](key: K): V
Reads m[key]: the same as get. A missing key reads as the default
of V and is not inserted.
Parameters
key— the key to look up
Returns
- the value for
key, or the default ofVwhen absent
operator []
[](key: K, value: V): V
Writes m[key] = value: the same as insert.
Parameters
key— the key to add or updatevalue— the value to store forkey
Returns
value, the value of the assignment expression
iterate
iterate(): OrderedMapIterator<K, V>
Returns an iterator over the entries as (key, value) tuples in
insertion order; for (var (key, value) in map) drives it.
The iterator holds a reference to the map. Changing a value through
an existing key while iterating is allowed; adding or removing an
entry makes the iterator’s next next() or current() panic.
Returns
- an iterator positioned before the first entry
copy
copy(): OrderedMap<K, V>
Returns a deep copy: a new map holding a copy of each key and value,
in the same order, made with copy. This is how copy(value)
copies an OrderedMap (the Copyable<OrderedMap<K, V>>
implementation); call copy(map) rather than this method.
Returns
- the copy
OrderedMapIterator
[Value] struct OrderedMapIterator<K, V>
An iterator over the entries of an OrderedMap as (key, value) tuples
in insertion order; returned by OrderedMap.iterate() and driven by
for (var (key, value) in map).
Holds a reference to the map. It is fail-fast: adding or removing an
entry after the iterator was created makes its next next() or
current() panic.
Type parameters
K— the key type of the mapV— the value type of the map
See also
OrderedMap.iterate
Constructor
OrderedMapIterator()
construct(source: OrderedMap<K, V>)
Creates an iterator positioned before the first entry of source.
Parameters
source— the map to iterate
Methods
next
next(): bool
Advances to the next entry.
Returns
- true when there is a current entry, false once every entry was visited
Panics
- if an entry was added or removed since the iterator was created (“OrderedMap modified during iteration”)
current
current(): (K, V)
Returns the entry the last successful next() moved to.
Returns
- the current entry as a
(key, value)tuple
Panics
- if an entry was added or removed since the iterator was created (“OrderedMap modified during iteration”)
- if
next()was never called or already returned false (“OrderedMapIterator.current() without an entry”)
BitSet
BitSet: a set of small non-negative integers, stored one bit each.
Source: runtime/bitset.pep
BitSet
struct BitSet
A growable set of non-negative integers, one bit per possible member.
Member n is bit n % 64 of word n / 64, so a set whose largest
member is m takes about m / 8 bytes whatever its count: a good fit for
visited flags, masks, and sieve or liveness tables over small integers.
For sparse or large integers use a HashSet<int>.
insert grows the set as needed and reserve grows it ahead of time;
the set never shrinks. contains and remove take O(1); count,
is_empty, ==, hash, iteration and the set operations take
O(capacity / 64).
Two sets are == when they hold the same members, whatever their
capacity, and equal sets hash equally, so a BitSet works as a
HashMap or HashSet key. A BitSet is not [Shared]: keep each one
on one thread.
Example
BitSet primes = [2, 3, 5, 7];
primes.insert(11);
assert(primes.contains(5), "a member");
assert(primes.count == 5, "five members");
for (var p in primes) {
print($"{p}"); # 2, 3, 5, 7, 11
}
Properties
capacity
var capacity: int { get; }
The number of members the set holds without growing, a multiple of 64.
count
var count: int { get; }
The number of members in the set. O(capacity / 64).
is_empty
var is_empty: bool { get; }
Whether the set has no members. O(capacity / 64).
Constructors
BitSet()
construct(capacity: int = 0)
Creates an empty set with room for the members 0 up to (not
including) capacity before it grows.
Parameters
capacity— the number of members to make room for; rounded up to a multiple of 64
Panics
- if
capacityis negative
BitSet()
[Implicit] construct(items: int[])
Creates a set holding each element of items.
The constructor is [Implicit]: an array converts to a set wherever
a set is expected, so an array literal can initialize one.
Parameters
items— the members to insert; duplicates are kept once
Panics
- if an element of
itemsis negative
Example
BitSet flags = [1, 4, 4];
assert(flags.count == 2, "4 is kept once");
Methods
reserve
reserve(capacity: int)
Makes room for the members 0 up to (not including) capacity, so
inserting them does not grow the set. Never shrinks it.
Parameters
capacity— the number of members to make room for
Panics
- if
capacityis negative
clear
clear()
Removes every member. The set keeps its capacity.
contains
contains(n: int): bool
Returns whether n is a member. O(1).
Parameters
n— the integer to look up; a negative one is never a member
Returns
- true when
nis a member
insert
insert(n: int): bool
Adds n to the set, growing it when n is past its capacity.
Parameters
n— the integer to add
Returns
- true when
nwas added, false when it was already a member
Panics
- if
nis negative
remove
remove(n: int): bool
Removes n from the set. Does nothing when n is not a member.
Parameters
n— the integer to remove
Returns
- true when
nwas a member and was removed, false otherwise
iterate
iterate(): BitSetIterator
Returns an iterator over the members in ascending order;
for (var n in set) drives it.
The iterator holds a reference to the set. Changing the set while iterating is safe: the iterator sees the members above its position as they are when it reaches them.
Returns
- an iterator positioned before the smallest member
union_with
union_with(other: BitSet)
Adds every member of other to this set.
Parameters
other— the set whose members to add
intersect_with
intersect_with(other: BitSet)
Keeps only the members that other also holds.
Parameters
other— the set to intersect with
difference_with
difference_with(other: BitSet)
Removes every member that other holds.
Parameters
other— the set whose members to remove
symmetric_difference_with
symmetric_difference_with(other: BitSet)
Keeps the members that exactly one of the two sets holds: removes
the members other also holds and adds those only other holds.
Parameters
other— the set to compare with
union
union(other: BitSet): BitSet
Returns a new set holding the members of either set.
Parameters
other— the other set
Returns
- the union of the two sets
intersection
intersection(other: BitSet): BitSet
Returns a new set holding the members both sets hold.
Parameters
other— the other set
Returns
- the intersection of the two sets
difference
difference(other: BitSet): BitSet
Returns a new set holding the members of this set that other does
not hold.
Parameters
other— the set whose members to leave out
Returns
- this set minus
other
symmetric_difference
symmetric_difference(other: BitSet): BitSet
Returns a new set holding the members that exactly one of the two sets holds.
Parameters
other— the other set
Returns
- the symmetric difference of the two sets
is_subset
is_subset(other: BitSet): bool
Returns whether every member of this set is also a member of other.
The empty set is a subset of every set.
Parameters
other— the set to compare with
Returns
- true when this set holds no member that
otherlacks
is_disjoint
is_disjoint(other: BitSet): bool
Returns whether the two sets have no member in common.
Parameters
other— the set to compare with
Returns
- true when no integer is a member of both sets
operator ==
==(rhs: BitSet): bool
Reports whether both sets hold the same members. Capacity is not compared.
Parameters
rhs— the set to compare with
Returns
- true when the sets have the same members
operator !=
!=(rhs: BitSet): bool
Reports whether the sets differ in any member; the negation of ==.
Parameters
rhs— the set to compare with
Returns
- true when one set holds a member the other lacks
copy
copy(): BitSet
Returns a new set with the same members and capacity. This is how
copy(value) copies a BitSet (the Copyable<BitSet>
implementation).
Returns
- the copy
hash
hash<BitSet>(value: BitSet): u64
Returns a hash of the members of value, so equal sets hash equally
whatever their capacity and a BitSet works as a HashMap or HashSet
key. O(capacity / 64).
Parameters
value— the set to hash
Returns
- the 64-bit hash of the set’s members
BitSetIterator
[Value] struct BitSetIterator
An Iterator<int> over the members of a BitSet in ascending order;
returned by BitSet.iterate() and driven by for (var n in set).
See also
BitSet.iterate
Constructor
BitSetIterator()
construct(source: BitSet)
Creates an iterator positioned before the smallest member of source.
Parameters
source— the set to iterate
Methods
next
next(): bool
Advances to the next larger member.
Returns
- true when there is a current member, false once every member was visited
current
current(): int
Returns the member the last successful next() moved to.
Returns
- the current member
Panics
- if
next()was never called or already returned false (“BitSetIterator.current() without a member”)
Deque
Deque: a double-ended queue over a growable ring buffer.
Source: runtime/deque.pep
Deque
struct Deque<T>
A double-ended queue: a sequence that grows and shrinks at both ends in amortized O(1).
Elements live in a ring buffer that doubles when full (starting at 4
slots), so push_front, push_back, pop_front and pop_back are
amortized O(1), and indexing is O(1) with index 0 at the front. Use it for
first-in first-out queues, breadth-first search, sliding windows and undo
history; an Array takes O(count) to remove its first element.
For a struct T the deque holds a counted reference to each element, as
an Array does: pushing acquires it, and popping hands the deque’s
reference to the caller. Deque is itself a struct, so assigning it
shares the same deque. A Deque is not [Shared]: keep each one on one
thread.
A Deque is Iterable<T>: for (var x in d) visits the elements from
front to back. Every change (a push, a pop, an element store, clear)
invalidates running iterators, and the next step of such a loop panics
with “Deque modified during iteration”. An array converts to a deque, so
an array literal can initialize one.
Type parameters
T— the element type
Example
Deque<int> queue = [1, 2];
queue.push_back(3);
queue.push_front(0);
assert(queue.pop_front() == 0, "front first");
assert(queue.pop_back() == 3, "then the back");
assert(queue[0] == 1 and queue.count == 2, "1 and 2 remain");
Properties
count
var count: ulong { get; }
The number of elements.
is_empty
var is_empty: bool { get; }
Whether the deque has no elements.
capacity
var capacity: ulong { get; }
The number of elements the deque holds without growing.
Constructors
Deque()
construct(capacity: ulong = 0)
Creates an empty deque with room for capacity elements before it
grows.
Parameters
capacity— the number of slots to allocate up front
Deque()
[Implicit] construct(items: T[])
Creates a deque holding the elements of items, front to back in
index order. The array is copied, not shared.
The constructor is [Implicit]: an array converts to a deque
wherever a deque is expected, so an array literal can initialize one.
Parameters
items— the elements, the first at the front
Example
Deque<String> names = ["ada", "grace"];
assert(names.front() == "ada", "the first element is the front");
Methods
reserve
reserve(capacity: ulong)
Makes room for capacity elements in all, so pushing up to that
many does not grow the deque. Never shrinks it.
Parameters
capacity— the number of elements to make room for
push_back
push_back(item: T)
Adds item at the back. Amortized O(1). Invalidates running
iterators.
Parameters
item— the element to add
push_front
push_front(item: T)
Adds item at the front, so it becomes element 0. Amortized O(1).
Invalidates running iterators.
Parameters
item— the element to add
pop_front
pop_front(): T
Removes and returns the front element. O(1). Invalidates running iterators.
Returns
- the element that was at the front
Panics
- if the deque is empty (“Deque pop_front on empty Deque”)
pop_back
pop_back(): T
Removes and returns the back element. O(1). Invalidates running iterators.
Returns
- the element that was at the back
Panics
- if the deque is empty (“Deque pop_back on empty Deque”)
front
front(): T
Returns the front element without removing it; the same as d[0].
Returns
- the element at index 0
Panics
- if the deque is empty (“Deque front on empty Deque”)
back
back(): T
Returns the back element without removing it; the same as
d[d.count - 1].
Returns
- the element at index
count - 1
Panics
- if the deque is empty (“Deque back on empty Deque”)
operator []
[](index: ulong): T
Returns the element at index, counting from 0 at the front; this is
the indexer read d[index]. O(1).
Parameters
index— the position to read
Returns
- the element at
index
Panics
- if
index < countdoes not hold (“Deque index read out of bounds”)
operator []
[](index: ulong, value: T): T
Replaces the element at index; this is the indexer write
d[index] = value. Invalidates running iterators.
Parameters
index— the position to overwritevalue— the new element
Returns
- the element now stored at
index(that is,value)
Panics
- if
index < countdoes not hold (“Deque index write out of bounds”)
clear
clear()
Removes every element, releasing each one. The deque keeps its capacity. Invalidates running iterators.
iterate
iterate(): DequeIterator<T>
Returns an iterator over the elements from front to back;
for (var x in d) drives it.
Returns
- an iterator positioned before the front element
DequeIterator
[Value] struct DequeIterator<T>
An Iterator<T> over the elements of a Deque from front to back;
returned by Deque.iterate() and driven by for (var x in d).
Type parameters
T— the element type of the deque
See also
Deque.iterate
Constructor
DequeIterator()
construct(source: Deque<T>)
Creates an iterator positioned before the front element of source.
Parameters
source— the deque to iterate
Methods
next
next(): bool
Advances to the next element.
Returns
- true when there is a current element, false once past the back
Panics
- if the deque changed since the iterator was created (“Deque modified during iteration”)
current
current(): T
Returns the element the last successful next() moved to.
Returns
- the current element
Panics
- if the deque changed since the iterator was created (“Deque modified during iteration”)
- if
next()was never called or already returned false (“DequeIterator.current() without an element”)
PriorityQueue
PriorityQueue: a binary heap that hands out its greatest element first.
Source: runtime/priority_queue.pep
PriorityQueue
struct PriorityQueue<T>
A priority queue: push adds elements in any order and pop removes
the greatest one.
A binary heap over an Array: push and pop take O(log count),
peek takes O(1), and building a queue from an array takes O(count).
Use it for Dijkstra’s and A* searches, schedulers, timers and top-k
selection.
Elements are ordered by compare<T> (numbers by value, String by
content, a Comparable struct by its compare), or by a comparer given
to the constructor, which returns a negative number, zero or a positive
number as its first argument sorts before, with or after its second. The
greatest element comes out first; reverse the comparer for the smallest
first. Elements that sort together come out in no particular order.
For a struct T the queue holds a counted reference to each element:
pushing acquires it, and popping hands the queue’s reference to the
caller. A PriorityQueue is not [Shared]: keep each one on one
thread.
Type parameters
T— the element type
Example
PriorityQueue<int> queue = [3, 1, 4];
queue.push(5);
assert(queue.pop() == 5, "the greatest first");
assert(queue.peek() == 4, "then 4");
# The smallest first, through a reversed comparer.
var earliest = PriorityQueue<int>((a, b) => b.compare(a));
earliest.push(30);
earliest.push(10);
assert(earliest.pop() == 10, "the smallest first");
Properties
count
var count: ulong { get; }
The number of elements.
is_empty
var is_empty: bool { get; }
Whether the queue has no elements.
Constructors
PriorityQueue()
construct()
Creates an empty queue ordered by compare<T>.
PriorityQueue()
construct(compare: (T, T) => int)
Creates an empty queue ordered by compare.
Parameters
compare— returns a negative number, zero or a positive number as its first argument sorts before, with or after its second
Example
var by_length = PriorityQueue<String>((a, b) => a.length.compare(b.length));
PriorityQueue()
[Implicit] construct(items: T[])
Creates a queue holding the elements of items, ordered by
compare<T>. O(count). The array is copied, not shared.
The constructor is [Implicit]: an array converts to a queue
wherever a queue is expected, so an array literal can initialize one.
Parameters
items— the elements, in any order
PriorityQueue()
construct(items: T[], compare: (T, T) => int)
Creates a queue holding the elements of items, ordered by
compare. O(count). The array is copied, not shared.
Parameters
items— the elements, in any ordercompare— returns a negative number, zero or a positive number as its first argument sorts before, with or after its second
Methods
push
push(item: T)
Adds item. O(log count), amortized.
Parameters
item— the element to add
pop
pop(): T
Removes and returns the greatest element. O(log count).
Returns
- the greatest element
Panics
- if the queue is empty (“PriorityQueue pop on empty PriorityQueue”)
peek
peek(): T
Returns the greatest element without removing it. O(1).
Returns
- the greatest element
Panics
- if the queue is empty (“PriorityQueue peek on empty PriorityQueue”)
clear
clear()
Removes every element, releasing each one.
into_sorted_array
into_sorted_array(): T[]
Removes every element and returns them sorted from smallest to greatest. O(count log count). The queue is left empty, keeping its comparer.
Returns
- the elements in ascending order
Example
PriorityQueue<int> queue = [2, 3, 1];
var sorted = queue.into_sorted_array(); # [1, 2, 3]
Iterators
Source: runtime/iterator.pep
Iterator
trait Iterator<T>
Something a for loop can step through, one element at a time.
for (var x in source) drives a source that declares next() and
current() directly: it calls next() before each pass and binds
current() to the loop variable while next() returns true. A type whose
for loop should go through a separate iterator implements Iterable<T>
instead. A type with neither protocol is a compile error in a for loop.
The loop goes by the methods a type has, so declaring the trait is
optional. Declaring it (extend Foo with Iterator<int>;) checks that
the methods match and lets the type be used as an Iterator<int> value,
which a for loop drives through the trait.
Type parameters
T— the type of the elements produced
See also
Iterable
Example
struct Countdown {
int remaining;
int now;
construct(this.remaining = 0) {}
bool next() {
if (remaining == 0) {
return false;
}
now = remaining;
remaining = remaining - 1;
return true;
}
int current() {
return now;
}
}
extend Countdown with Iterator<int>;
main(): int {
var total = 0;
for (int n in Countdown(3)) {
total = total + n; # visits 3, 2, 1
}
return total;
}
Methods
next
next(): bool
Advances to the next element.
The first call moves to the first element, if any. Once it returns false it must keep returning false.
Returns
- true if there is a current element, false when the elements are exhausted
current
current(): T
Returns the element the last successful next() moved to.
Calling it before the first next(), or after next() returned false,
is an error; the runtime’s iterators panic in that case.
Returns
- the current element
map
map<U>(transform: (T) => U): MapIterator<T, U>
Returns an iterator over transform applied to each element, in order.
Lazy: transform runs once per element, as the result is advanced, and
the result advances this iterator. Every type that declares
Iterator<T> gets this method.
Type parameters
U— the typetransformreturns
Parameters
transform— the function applied to each element
Returns
- a
MapIterator<T, U>over the transformed elements
Example
var names = Array<String>();
for (var size in names.iterate().map((name) => name.size)) {
print(size);
}
filter
filter(keep: (T) => bool): FilterIterator<T>
Returns an iterator over the elements for which keep returns true, in
order.
Lazy: keep runs once per element, as the result is advanced, and the
result advances this iterator. Every type that declares Iterator<T>
gets this method.
Parameters
keep— the test an element must pass
Returns
- a
FilterIterator<T>over the elements that pass
Example
var nums = Array<int>();
for (var even in nums.iterate().filter((n) => n % 2 == 0)) {
print(even);
}
enumerate
enumerate<TIndex=ulong>(): EnumerateIterator<T, TIndex>
Returns an iterator over the elements paired with their positions:
(0, first), (1, second), and so on.
Lazy, like map. Destructure the pair in the loop to name both parts.
The positions count in ulong, like Array.count; name another
integer type to count in it (enumerate<int>()). Every type that
declares Iterator<T> gets this method.
Type parameters
TIndex— the integer type of the positions,ulongby default
Returns
- an
EnumerateIterator<T, TIndex>over(index, element)tuples
Example
var names = ["Ada", "Grace"];
for (var (i, name) in names.iterate().enumerate()) {
print($"{i}: {name}"); # 0: Ada, then 1: Grace
}
zip
zip<U>(other: Iterator<U>): ZipIterator<T, U>
Returns an iterator over pairs of this iterator’s elements and
other’s, in step: (first, other's first), and so on. It stops when
either runs out.
Lazy, like map. Every type that declares Iterator<T> gets this
method.
Type parameters
U— the type ofother’s elements
Parameters
other— the iterator whose elements form the second half of each pair
Returns
- a
ZipIterator<T, U>over(element, other element)tuples
Example
var names = ["Ada", "Grace"];
var ages = [36, 45];
for (var (name, age) in names.iterate().zip(ages.iterate())) {
print($"{name} is {age}");
}
to_array
to_array(): Array<T>
Returns a new Array holding the remaining elements, in order.
Runs the iteration to the end. Like the adapters, it works on a copy
of the iterator, so a value iterator (every runtime iterator is one)
is left where it was. Every type that declares Iterator<T> gets this
method.
Returns
- the elements, possibly none
Example
var evens = (1..>10).filter((n) => n % 2 == 0).to_array(); # [2, 4, 6, 8, 10]
MapIterator
[Value] struct MapIterator<T, U>
The iterator Iterator.map returns: each element of the source iterator,
passed through the transform function.
Type parameters
T— the type of the source’s elementsU— the type of the elements produced
See also
Iterator.map
Constructor
MapIterator()
construct(source: Iterator<T>, transform: (T) => U)
Makes an iterator over transform applied to the elements of source.
Parameters
source— the iterator to transformtransform— the function applied to each element
Methods
next
next(): bool
Advances to the next element, and applies the transform function to it.
Returns
- true if there is a current element, false when the source is exhausted
current
current(): U
Returns the transformed element the last successful next() produced.
Returns
- the current element
FilterIterator
[Value] struct FilterIterator<T>
The iterator Iterator.filter returns: the elements of the source
iterator that the test function accepts.
Type parameters
T— the type of the elements
See also
Iterator.filter
Constructor
FilterIterator()
construct(source: Iterator<T>, keep: (T) => bool)
Makes an iterator over the elements of source that keep accepts.
Parameters
source— the iterator to filterkeep— the test an element must pass
Methods
next
next(): bool
Advances to the next element that the test function accepts.
Returns
- true if there is a current element, false when the source is exhausted
current
current(): T
Returns the element the last successful next() moved to.
Returns
- the current element
EnumerateIterator
[Value] struct EnumerateIterator<T, TIndex>
The iterator Iterator.enumerate returns: each element of the source
iterator, paired with its position.
Type parameters
T— the type of the source’s elementsTIndex— the integer type of the positions
See also
Iterator.enumerate
Constructor
EnumerateIterator()
construct(source: Iterator<T>)
Makes an iterator over the elements of source, numbered from 0.
Parameters
source— the iterator to number
Methods
next
next(): bool
Advances to the next element.
Returns
- true if there is a current element, false when the source is exhausted
current
current(): (TIndex, T)
Returns the element the last successful next() moved to, with its
index.
Returns
- the current
(index, element)pair
ZipIterator
[Value] struct ZipIterator<T, U>
The iterator Iterator.zip returns: the elements of two iterators,
paired in step, until either runs out.
Type parameters
T— the type of the first iterator’s elementsU— the type of the second iterator’s elements
See also
Iterator.zip
Constructor
ZipIterator()
construct(first: Iterator<T>, second: Iterator<U>)
Makes an iterator over the elements of first and second, paired.
Parameters
first— the iterator giving the first half of each pairsecond— the iterator giving the second half of each pair
Methods
next
next(): bool
Advances both iterators. second is not advanced once first has
run out.
Returns
- true if both have a current element, false when either is exhausted
current
current(): (T, U)
Returns the pair the last successful next() moved to.
Returns
- the current
(first element, second element)pair
Iterable
trait Iterable<T>
Something that hands out an Iterator<T> over its elements, such as
Array<T> and String (which yields one code point at a time, as a
String).
for (var x in source) calls source.iterate() once, then drives the
returned iterator with next() and current(). When a type declares both
iterate() and next()/current(), the loop uses iterate().
Type parameters
T— the type of the elements produced
See also
Iterator
Example
var nums = Array<int>();
nums.append(1);
nums.append(2);
var total = 0;
for (var x in nums) {
total = total + x;
}
Methods
iterate
iterate(): Iterator<T>
Returns a new iterator positioned before the first element.
The result may be any type that satisfies Iterator<T> (for example
ArrayIterator<T>); each call starts a fresh, independent pass.
Returns
- an iterator over the elements
Scoped
The scope protocol behind with (var x = scope) { ... } else { ... }.
A with block holds scope in a hidden local, binds x to
scope.enter(), and runs the body. When enter() returns a struct
reference that is null, the body is skipped and the optional else block
runs instead. Both locals are released on every path out of the block
(falling off the end, return, break, continue), the binding first.
Cleanup is the scope object’s destructor, which calls exit().
Source: runtime/scoped.pep
Scoped
trait Scoped<T>
Something a with block can enter.
enter() runs once when the block is entered, and its result is what the
header binds. exit() is the cleanup: implementations call it from their
destructor (destruct { this.exit(); }), so it runs when the scope
object’s last reference is released. For a scope object created in the
with header that is the end of the block; for one that is also held
elsewhere, it is when that last owner lets go.
A with block with no binding (with (scope) { ... }) still holds the
result of enter() until the block ends. An enter() that returns void
has nothing to bind.
Type parameters
T— the typeenter()returns
Example
var open = 0;
struct Session {
construct() {
open = open + 1;
}
destruct {
this.exit();
}
Session enter() {
return this;
}
exit() {
open = open - 1;
}
}
extend Session with Scoped<Session>;
main(): int {
with (var session = Session()) {
# open == 1 here
}
return open; # 0: the session was destroyed at the end of the block
}
Methods
enter
enter(): T
Enters the scope.
A null struct reference means there is nothing to use: the with
body is skipped and its else block runs instead.
Returns
- the value the
withheader binds
exit
exit()
Leaves the scope. Implementations call it from their destructor.
Weak
Weak references: Weak<T> points at an object without keeping it alive.
Source: runtime/weak.pep
Weak
struct Weak<T>
A reference to a struct object, or to the struct behind a trait value
(Weak<Shape>), that does not keep it alive, for back
pointers (a child’s parent, an observer’s subject) that would otherwise
form a reference cycle and leak.
The object is reached only through a with block, which upgrades the weak
reference to an ordinary strong one for the length of the block. Once the
object has been destroyed the upgrade fails: the body is skipped and the
else block runs instead.
A Weak<T> never changes what it points at, so copies of it can be shared
freely.
Type parameters
T— the struct or trait type pointed at
See also
Scoped
Example
struct Child {
Weak<Parent> owner;
construct(Parent owner) {
this.owner = Weak<Parent>(owner);
}
String owner_name() {
with (var parent = this.owner) {
return parent.name;
} else {
return "(gone)";
}
return "";
}
}
Constructor
Weak()
construct(target: T)
Creates a weak reference to target’s object.
Parameters
target— the object to point at; a null reference makes a weak reference that is always expired
Methods
expired
expired(): bool
Reports whether the object has been destroyed.
A false result can be out of date by the time it is used; enter the
reference with with to use the object.
Returns
- true once the object has been destroyed, or if the reference was made from null
enter
enter(): T
Upgrades the weak reference for a with block.
Returns
- a new strong reference to the object, or null once it has been destroyed
exit
exit()
Leaves the scope. The strong reference enter() returned is released
by the with block itself, so there is nothing to do.
Handle
Handle<T>: owns a resource that Pepper’s reference counting does not
know about, such as a C library’s object, and releases it when the last
reference to the handle goes away, like C#’s SafeHandle.
Source: runtime/handle.pep
Handle
struct Handle<T>
Owns a raw resource and calls a release function on it once, when the
handle is destroyed or release() is called, whichever comes first.
Use it to tie a C library’s object (a __ptr<void> from an extern, a
file descriptor) to Pepper’s lifetimes: hold a Handle in a struct
field, and the resource is freed when the struct is. A handle is a
reference like any struct, so copies share one resource, and it is
released when the last copy goes away. detach() takes the resource
back without releasing it.
release is an ordinary function value, so a closure may capture what
the release needs (such as an owner that must outlive the resource:
holding it in the closure keeps it alive until the release runs).
Type parameters
T— the type of the raw resource, usually__ptr<void>or an integer
Example
extern void* malloc(ulong size);
extern void free(void* p);
main(): int {
var buffer = Handle<__ptr<void>>(malloc(16), (p) => free(p));
# use buffer.value; free runs when `buffer` goes out of scope
return 0;
}
Properties
value
var value: T { get; }
The resource. It stays readable after the handle is released or detached, but it no longer refers to anything the handle owns.
Constructor
Handle()
construct(value: T, release: (T) => void)
Creates a handle that owns value and calls release(value) when
it is destroyed.
Parameters
value— the resource to ownrelease— the function that freesvalue; it is called at most once
Methods
is_owned
is_owned(): bool
Reports whether the handle still owns its resource: false after
release() or detach().
Returns
- true while the resource has not been released or detached
release
release()
Releases the resource now instead of when the handle is destroyed.
Calling it again, or after detach(), does nothing.
detach
detach(): T
Gives up ownership without releasing the resource, which the caller then frees itself.
Returns
- the resource
Result
Result<T, E>: the value of an operation that can fail, either Ok
holding its value or Err holding its error, like Rust’s Result.
Source: runtime/result.pep
Error
struct Error
The default error type of Result: a human-readable message.
Example
var r = Result<int>.err(Error("boom"));
var n = r.error().message.length; # 4
Fields
message
String message
The description of what went wrong; empty when none was given.
Constructor
Error()
[Implicit] construct(message: String = "")
Creates an Error carrying message.
The constructor is [Implicit]: a String converts to an Error
wherever an Error is expected, as in Result<int>.err("bad input").
Parameters
message— the description of the failure; defaults to the empty string
Result
[Value] enum Result<TValue, TError=Error>
The outcome of an operation that can fail: either Ok, holding a TValue,
or Err, holding a TError, like Rust’s Result<T, E>.
An enum with the cases Ok(value) and Err(error). Build one with a
case (Result<int>.Ok(42)) or the static ok or err functions, and
read it with match or is (if (r is Err(e)) { ... }), or with
is_ok(), is_err(), value() and error(); value() and error()
panic on the wrong kind of result. TError defaults to Error, so
Result<int> is Result<int, Error>.
Result<void> is the result of an operation that returns nothing on
success, like Rust’s Result<(), E>. A void type argument erases what
it types: that instance’s Ok carries nothing (Result<void>.Ok, and
the pattern Ok), ok() takes no argument, and value() returns
nothing, so it only checks that the result is Ok.
Type parameters
TValue— the type of the value an Ok result carries;voidwhen there is noneTError— the type of the error an Err result carries; defaults toError
Example
main(): int {
var a = Result<int>.ok(42);
if (a.value() != 42) { return 1; }
var b = Result<int>.err(Error("boom"));
if (!b.is_err()) { return 2; }
var n = match (b) { Ok(v) => v, Err(e) => e.message.length }; # 4
return b.ok_or(0); # 0
}
Cases
Ok
Ok(value: TValue)
The operation succeeded with value.
Parameters
value— the success value
Err
Err(error: TError)
The operation failed with error.
Parameters
error— the failure value
Static methods
ok
static ok(v: TValue): Result<TValue, TError>
Returns an Ok result holding v; the same as Result.Ok(v).
For a struct TValue the result acquires a reference to v.
For Result<void>, ok() takes no argument.
Parameters
v— the success value
Returns
- an Ok result whose
value()isv
Example
var r = Result<int>.ok(42);
var done = Result<void>.ok();
err
static err(e: TError): Result<TValue, TError>
Returns an Err result holding e; the same as Result.Err(e).
For a struct TError the result acquires a reference to e.
Parameters
e— the failure value
Returns
- an Err result whose
error()ise
Example
var r = Result<int>.err(Error("boom"));
Methods
is_ok
is_ok(): bool
Reports whether this result is Ok.
Returns
- true when the result holds a value
is_err
is_err(): bool
Reports whether this result is Err.
Returns
- true when the result holds an error
value
value(): TValue
Returns the success value, panicking if this result is Err.
For a struct TValue the caller gets its own reference; the result
keeps its reference too.
For Result<void> it returns nothing: calling it only checks that the
result is Ok.
Returns
- the value of an Ok result
Panics
- if the result is Err (“Result.value on an Err value”)
Example
var n = Result<int>.ok(42).value(); # 42
error
error(): TError
Returns the failure value, panicking if this result is Ok.
For a struct TError the caller gets its own reference; the result
keeps its reference too.
Returns
- the error of an Err result
Panics
- if the result is Ok (“Result.error on an Ok value”)
Example
var e = Result<int>.err(Error("boom")).error(); # e.message == "boom"
ok_or
ok_or(fallback: TValue): TValue
Returns the success value, or fallback if this result is Err.
Never panics. fallback is evaluated by the caller whether or not it
is used.
Parameters
fallback— the value to return when the result is Err
Returns
- the value when the result is Ok, otherwise
fallback
Example
var n = Result<int>.err(Error("e")).ok_or(-1); # -1
Optional
Source: runtime/optional.pep
Optional
[Value] enum Optional<T>
A value of type T that may be absent: either Value, holding a T, or
Empty, holding nothing, like Rust’s Option<T> and Swift’s Optional.
Use it instead of a null reference or a sentinel (-1, "") wherever a
value may be missing: the type says so, and reading the value goes through
value(), which checks, or value_or(), which cannot fail. It works for
every type, including those that have no null, such as int.
T? is shorthand for Optional<T>, and a variable or field of an
optional type that is not given a value starts as Empty.
Build one with its cases (Optional<int>.Value(1), Optional<int>.Empty).
Test it with is_value() or is_empty(), as a condition by itself (if (found)), or with match
and is on its cases (if (found is Value(v))).
An optional is a value enum: it is stored
inline in whatever holds it, with no allocation, and copying one gives an
independent optional. It is never null, and has no identity, so compare
optionals with ==, not ===.
Type parameters
T— the type of the value a Value optional holds
Example
find_even(values: Array<int>): int? {
for (var v in values) {
if (v % 2 == 0) { return Optional<int>.Value(v); }
}
return Optional<int>.Empty;
}
main(): int {
var values = Array<int>();
values.append(3);
values.append(4);
var found = find_even(values);
if (!found) { return 1; }
return found.value() - 4; # 0
}
Cases
Empty
Empty
Holds no value. It is the first case, so an optional that is never given a value is Empty.
Value
Value(value: T)
Holds a value.
Parameters
value— the value held
Static methods
some
[Deprecated("use the case 'Value' instead", "Value")] static some(value: T): Optional<T>
Deprecated: use the Value case, Optional<T>.Value(value),
instead.
Returns a Value optional holding value.
Parameters
value— the value to hold
Returns
- a Value optional whose value is
value
none
[Deprecated("use the case 'Optional<T>.Empty' instead")] static none(): Optional<T>
Deprecated: use the Empty case, Optional<T>.Empty, instead.
Returns an Empty optional, one holding no value.
Returns
- an Empty optional
Methods
is_value
is_value(): bool
Reports whether this optional holds a value.
Returns
- true when the optional is Value
Example
var found = Optional<int>.Value(7);
if (found.is_value()) { print(found.value()); } # prints 7
is_empty
is_empty(): bool
Reports whether this optional holds no value.
Returns
- true when the optional is Empty
Example
var missing = Optional<int>.Empty;
if (missing.is_empty()) { print("missing"); }
is_some
[Deprecated("use 'is_value' instead", "is_value")] is_some(): bool
Deprecated: use is_value instead.
Reports whether this optional holds a value.
Returns
- true when the optional is Value
is_none
[Deprecated("use 'is_empty' instead", "is_empty")] is_none(): bool
Deprecated: use is_empty instead.
Reports whether this optional holds no value.
Returns
- true when the optional is Empty
operator !!
!!(): bool
Reports whether this optional holds a value: the truth test, so an optional can stand as a condition by itself.
if (found) means if (found.is_value()), !found means
found.is_empty(), and the same holds in while, &&, || and
?:.
Returns
- true when the optional is Value
Example
var found = Optional<int>.Value(7);
if (found) { print(found.value()); } # prints 7
value
value(): T
Returns the value, panicking if this optional is Empty.
For a struct T the caller gets its own reference; the optional keeps
its reference too.
Returns
- the value of a Value optional
Panics
- if the optional is Empty (“Optional.value on an Empty optional”)
Example
var n = Optional<int>.Value(42).value(); # 42
value_or
value_or(fallback: T): T
Returns the value, or fallback if this optional is Empty.
Never panics. fallback is evaluated by the caller whether or not it
is used.
Parameters
fallback— the value to return when the optional is Empty
Returns
- the value when the optional is Value, otherwise
fallback
Example
var n = Optional<int>.Empty.value_or(-1); # -1
Copying
Deep copies: copy(value) makes a copy that shares no object that is
not thread-safe with the original, so it can be handed to another thread;
Copyable<T> lets a type say how it is copied.
Source: runtime/copy.pep
copy
copy<T>(value: T): T
Returns a copy of value that shares no non-thread-safe object with it,
so the copy can be handed to another thread.
Every struct reachable from value whose type is not thread-safe is
copied once. Objects referenced twice stay shared inside the copy, and
cycles are preserved. Thread-safe objects ([Shared] structs, shared
function values) are not copied: the copy references the same ones. A
weak reference to a copied object points at its copy; a weak reference to
an object outside the copied value becomes null.
A struct is copied field by field, without running its constructor,
unless it implements Copyable<T>. A struct with a destructor or a raw
pointer field must implement Copyable<T> to be copied, since copying
its fields would share what it owns; String, Array, HashMap and
HashSet do. Copying such a struct that does not, or a plain function
value, is a compile error.
Called inside a Copyable<T>.copy() implementation while a copy is
running, copy continues that copy: an object it already copied is not
copied again.
A trait value is copied by copying the struct behind it, which is known
only at run time: when that struct cannot be copied, copy panics.
Runs in O(size of the value) and allocates each copied object.
Type parameters
T— the type of the value
Parameters
value— the value to copy
Returns
- a copy that shares no non-thread-safe object with
value;valueitself whenTis thread-safe
Panics
- if
valuereaches a trait value holding a struct that cannot be copied
Example
var names = Array<String>();
names.append("ada");
var copied = copy(names);
copied[0].append(" lovelace"); # names[0] is still "ada"
register_copy
register_copy<T>(original: T, made: T)
Records made as the copy of original in the running copy, so that
copying original again returns made. A Copyable<T>.copy()
implementation calls it before copying the values it holds when one of
them may lead back to it; outside a copy it does nothing.
Type parameters
T— the type of the value, a struct
Parameters
original— the object being copiedmade— its copy
Example
copy(): Node {
var made = Node(value);
register_copy(this, made); # a cycle back to this finds made
made.next = copy(next);
return made;
}
move
move<T>([MoveSource] value: T): T
Hands over the reference value holds and leaves it null, with no copy
and no change to any count: the result is the same object.
value must be a local variable (or a [Move] parameter) that holds
the only reference to its value: nothing else may refer to it or to
anything it reaches. The compiler checks that, and that the variable is
not used again until it is assigned a new value. Use it to pass a value
you built to a parameter that takes a new value, such as Channel.send
or Mutex.replace, without the cost of copy.
Type parameters
T— the type of the value
Parameters
value— the variable to move from
Returns
- the value
valueheld, which nothing else references
Example
var lines = Array<String>();
lines.append("first");
var log = async.Mutex<Array<String>>(move(lines)); # lines is null now
Copyable
trait Copyable<T>
A type that says how copy copies it, for a struct that copy cannot
copy field by field: one with a destructor or a raw pointer field, such as
a container that owns a buffer.
Declare it with extend Buffer with Copyable<Buffer>; and give the
struct a copy(): Buffer method. The method builds the copy and copies
each struct value it holds with copy(value), which, called while a copy
is running, reuses a copy already made, so objects referenced twice stay
shared.
Type parameters
T— the implementing type
Example
struct Buffer {
__ptr<u8> data;
ulong size;
construct(ulong size = 0) {
data = _pep_malloc(size);
this.size = size;
}
destruct {
_pep_free(data);
}
copy(): Buffer {
var made = Buffer(size);
var i: ulong = 0;
while (i < size) {
made.data[i] = data[i];
i = i + 1;
}
return made;
}
}
extend Buffer with Copyable<Buffer>;
Methods
copy
copy(): T
Returns a copy of this value, for copy.
Copy each struct value this one holds with copy(value). A value
that can hold a reference back to itself calls
register_copy(this, made) before copying what it holds, so the
cycle leads back to the copy. Call copy(x), not x.copy(), to copy
a Copyable value: copy(x) keeps objects shared inside one copy.
Returns
- the copy
Copies
[Deprecated("implement 'Copyable<T>' as 'copy(): T', which copies what it holds with 'copy(value)'")] type Copies = _Copies
Deprecated: implement Copyable<T> as copy(): T, which copies what it
holds with copy(value) and register_copy(original, made).
The context of a running copy, which a Copyable<T> implementation once
took as copy(copies: Copies): T. Such an implementation still works
for this release.
URI
URI references (RFC 3986): parse any URI, such as https://example.com/a?b#c,
mailto:someone@example.com or /items?page=2, into its parts.
Source: runtime/uri.pep
Uri
struct Uri
A URI reference split into its RFC 3986 components: scheme, authority (user information, host and port), path, query and fragment.
Uri.parse accepts any scheme, and a reference with no scheme (a
relative reference, such as /items?page=2, which is what an HTTP
server sees as a request’s target). The components keep their
percent-escapes as written; Uri.decode turns an escaped component into
text. The scheme is lowercased, since schemes are case-insensitive;
nothing else is changed. to_string() puts the parts back together.
A component that can be absent is optional, so an empty one (http://h/?
has an empty query) differs from a missing one (http://h/ has none).
Example
var uri = Uri.parse("HTTPS://user@example.com:8443/a%20b?x=1#top").value();
assert(uri.scheme == "https", "the scheme is lowercased");
assert(uri.user_info.value() == "user", "user information");
assert(uri.host.value() == "example.com", "host");
assert(uri.port.value() == 8443, "port");
assert(uri.path == "/a%20b", "the path keeps its escapes");
assert(Uri.decode(uri.path).value() == "/a b", "decoded");
assert(uri.query.value() == "x=1" && uri.fragment.value() == "top", "query and fragment");
Fields
scheme
var scheme: String
The scheme, lowercased, such as https; empty for a relative reference.
user_info
var user_info: String?
The user information before @ in the authority, if any.
host
var host: String?
The host, without the brackets of an IPv6 address; Empty when the URI
has no authority (no //), and empty for one such as file:///tmp.
An IPv6 scope keeps its escaped % (RFC 6874): the host of
http://[fe80::1%25eth0]/ is fe80::1%25eth0, which Uri.decode
turns into the fe80::1%eth0 that io.IpAddress.parse reads.
port
var port: u16?
The port after the host, if the authority gives one.
path
var path: String
The path, possibly empty, with its percent-escapes.
query
var query: String?
The query after ?, without the ?, if any.
fragment
var fragment: String?
The fragment after #, without the #, if any.
Constructor
Uri()
construct()
Creates an empty relative reference: no scheme, no authority, and an empty path.
Static methods
parse
static parse(text: String): Result<Uri>
Parses text as a URI reference: an absolute URI with any scheme,
or a relative reference.
Characters outside printable ASCII (spaces, controls, and every
non-ASCII byte) must be percent-encoded, and each % must be
followed by two hexadecimal digits.
Parameters
text— the URI reference
Returns
- the parsed URI, or an error saying what is wrong: an invalid scheme, a character that must be encoded, a bad escape, an IPv6 host with no
], or a port that is not a number from 0 to 65535
Example
var mail = Uri.parse("mailto:someone@example.com").value();
assert(mail.scheme == "mailto" && mail.host.is_empty(), "no authority");
assert(mail.path == "someone@example.com", "the path holds the address");
assert(Uri.parse("a b").is_err(), "a space must be encoded");
decode
static decode(text: String): Result<String>
Returns text with each percent-escape (%20) replaced by the byte
it stands for, decoded as UTF-8. A + is kept as it is; a query that
uses + for a space needs that replaced first.
Parameters
text— a component with percent-escapes, such aspath
Returns
- the decoded text, where each byte sequence that is not valid UTF-8 becomes U+FFFD, the replacement character, or an error when a
%is not followed by two hexadecimal digits
Example
assert(Uri.decode("caf%C3%A9%21").value() == "café!", "decoded");
assert(Uri.decode("100%").is_err(), "a lone '%'");
encode
static encode(text: String): String
Returns text with each byte outside RFC 3986’s unreserved set
(A-Z a-z 0-9 - . _ ~) written as a percent-escape, so the result is
safe as one path segment or one query value: /, ?, &, = and
% are escaped too. decode reverses it.
Parameters
text— the component to escape, such as a file name or a search term
Returns
- the escaped text, with uppercase hexadecimal digits
Example
assert(Uri.encode("a b/c&d") == "a%20b%2Fc%26d", "escaped");
assert(Uri.encode("café") == "caf%C3%A9", "UTF-8 bytes");
encode_form
static encode_form(pairs: Array<(String, String)>): String
Returns pairs as an application/x-www-form-urlencoded body or
query, the way an HTML form sends them: name=value joined with
&, each side escaped like encode except that a space is +.
Parameters
pairs— the names and values, in order; a name may repeat
Returns
- the encoded text, such as
q=pepper+lang&page=2
Example
assert(Uri.encode_form([("q", "pepper lang"), ("page", "2")]) == "q=pepper+lang&page=2", "a query");
decode_form
static decode_form(text: String): Result<Array<(String, String)>>
Returns the names and values of an application/x-www-form-urlencoded
body or query, such as a request’s query, in order. Each + is a
space and percent-escapes are decoded; a pair with no = has an empty
value, and empty pairs (a=1&&b=2) are skipped.
Parameters
text— the encoded pairs, such asq=pepper+lang&page=2
Returns
- the decoded pairs, or an error when a
%is not followed by two hexadecimal digits
Example
var pairs = Uri.decode_form("q=pepper+lang&page=2").value();
assert(pairs.count == 2 && pairs[0].1 == "pepper lang", "decoded");
Methods
is_absolute
is_absolute(): bool
Returns whether this URI has a scheme (an absolute URI rather than a relative reference).
Returns
- true when
schemeis not empty
authority
authority(): String?
Returns the authority as it appears after //: user information,
host (in brackets when it is an IPv6 address) and port.
Returns
- the authority, such as
user@example.com:8443, or Empty whenhostis Empty
path_and_query
path_and_query(): String
Returns the path and query, as an HTTP request line names its target:
the path (/ when it is empty), then ? and the query if there is
one. The fragment is left out.
Returns
- the path and query, such as
/items?page=2
resolve
resolve(reference: Uri): Uri
Resolves reference against this URI, its base, as RFC 3986
(section 5.2) describes and as a browser resolves a link on a page:
an absolute reference stands as it is, //host/path keeps only the
base’s scheme, /path keeps its authority too, and a relative path
replaces the base’s last segment. . and .. segments are removed.
The fragment is always the reference’s.
Parameters
reference— the reference to resolve, such as an HTTPLocation
Returns
- the resolved URI
Example
var base = Uri.parse("https://example.com/docs/guide/intro?x=1").value();
assert(base.resolve(Uri.parse("next").value()).to_string() == "https://example.com/docs/guide/next", "a sibling");
assert(base.resolve(Uri.parse("../api").value()).to_string() == "https://example.com/docs/api", "up one");
assert(base.resolve(Uri.parse("/").value()).to_string() == "https://example.com/", "the root");
assert(base.resolve(Uri.parse("?y=2").value()).to_string() == "https://example.com/docs/guide/intro?y=2", "a new query");
to_string
to_string(): String
Returns the URI as text, putting its components back together. For a parsed URI this is the text it was parsed from, except that the scheme is lowercased.
Returns
- the URI reference as a String
Test checks
Checks for tests: expect_equal, expect_not_equal, expect_true and
expect_false, which every program can call without an import. Each
panics when its check fails, as assert does, but reports the values it
compared, at the line of the call:
math_test.pep:12:5: panic: expect_equal failed
actual: 3
expected: 4
A [Test] function fails on that panic (see the testing chapter).
Source: runtime/expect.pep
expect_equal
expect_equal<T>(actual: T, expected: T)
Panics unless actual == expected, reporting both values.
Type parameters
T— the type of the values; it needs==and a text form (seeto_string)
Parameters
actual— the value the code under test producedexpected— the value it should have produced
Panics
- if the values differ: prints the location of the call and both values, and aborts (see
panic)
Example
var numbers = Array<int>();
numbers.append(1);
expect_equal(numbers.count, 1);
expect_not_equal
expect_not_equal<T>(actual: T, unexpected: T)
Panics if actual == unexpected, reporting the value.
Type parameters
T— the type of the values; it needs==and a text form (seeto_string)
Parameters
actual— the value the code under test producedunexpected— the value it must not be
Panics
- if the values are equal: prints the location of the call and the value, and aborts (see
panic)
Example
expect_not_equal("a".repeat(2), "a");
expect_true
expect_true(condition: bool)
Panics unless condition holds. Unlike assert, it needs no message:
the location names the check.
Parameters
condition— the condition that must hold
Panics
- if
conditionis false: prints the location of the call, and aborts (seepanic)
Example
expect_true("pepper".starts_with("pep"));
expect_false
expect_false(condition: bool)
Panics if condition holds.
Parameters
condition— the condition that must not hold
Panics
- if
conditionis true: prints the location of the call, and aborts (seepanic)
Example
expect_false("pepper".is_empty);
The io module
Files, directories, paths, and sockets, modeled on Rust’s std::fs,
std::path and std::net.
Unlike the rest of the standard library, io is a module: bring it in with
import io (and write io.File, io.Path) or import names directly with
import [File, Path] from io. Every fallible operation returns an
IoResult<T>, a Result whose error is an
IoError.
- Path: building, splitting, and inspecting paths
- File: opening, reading, writing, and locking files, whole-file helpers, and
FileTransfer - Directory: listing, creating, and removing directories
- Network addresses:
IpAddress,SocketAddress, andresolve - Sockets:
TcpListener,Incoming,SharedTcpListener,TcpStream,SocketTransfer, andUdpSocket - Unix domain sockets:
UnixListener,SharedUnixListener,UnixStream, andUnixDatagram - Streams: the
ReaderandWritertraits,lines()andchunks() - Streams in memory:
ByteReaderandByteWriter - Print: deprecated; use
Writer - Errors and platform types:
IoError,ErrorKind,IoResult, and handle types
Example
import io;
main(): int {
var path = io.Directory.temp().join("greeting.txt");
assert(io.File.write_text(path, "hello").is_ok(), "write");
assert(io.File.read_to_string(path).value() == "hello", "read back");
io.File.remove(path);
return 0;
}
Source: runtime/io/module.pep
Errors and platform types
Source: runtime/io/types.pep
FileHandle
[Target(!OS(Windows))] type FileHandle = int
The raw operating-system handle of an open file: a POSIX file descriptor.
File.Handle holds one. A valid descriptor is >= 0; a File that is
closed (or was never opened) holds -1. The standard streams are
descriptors 0, 1 and 2. On Windows this is a HANDLE instead.
See also
File
Note
- code that must build for every target should not rely on this being an
int
SocketHandle
[Target(!OS(Windows))] type SocketHandle = int
The raw operating-system handle of a socket: a POSIX file descriptor.
TcpListener, TcpStream and UdpSocket hold one. A valid descriptor
is >= 0; a closed socket holds -1. On Windows this is a SOCKET
instead.
See also
TcpStream
Note
- code that must build for every target should not rely on this being an
int
ErrorCode
[Target(!OS(Windows))] type ErrorCode = int
The raw platform error code carried by IoError.code: a POSIX errno
value such as ENOENT (2). On Windows this is a DWORD from
GetLastError() instead.
The numbers differ between platforms (and a few between Linux and
macOS), so portable code classifies an error with IoError.kind() or
its predicates (is_not_found(), …) rather than comparing codes.
See also
IoError
Mode
[Target(!OS(Windows))] type Mode = uint
File mode bits as reported in Metadata.Permissions: on POSIX the
mode_t permission bits mode & 07777 (the rwx bits for owner, group
and others plus setuid, setgid and sticky; the file-type bits are
removed). On Windows the same field holds FILE_ATTRIBUTE_* bits
instead.
See also
Metadata
FileHandle
[Target(OS(Windows))] type FileHandle = __ptr<void>
The raw operating-system handle of an open file: a Windows HANDLE.
File.Handle holds one. A File that is closed (or was never opened)
holds null; neither null nor INVALID_HANDLE_VALUE (-1) is a valid
handle. The standard streams are the handles from GetStdHandle. On
POSIX this is an int file descriptor instead.
See also
File
Note
- code that must build for every target should not rely on this being a
__ptr<void>
SocketHandle
[Target(OS(Windows))] type SocketHandle = ulong
The raw operating-system handle of a socket: a Winsock SOCKET, an
unsigned pointer-sized integer.
TcpListener, TcpStream and UdpSocket hold one. A closed socket
holds INVALID_SOCKET (all bits set). On POSIX this is an int file
descriptor instead.
See also
TcpStream
Note
- code that must build for every target should not rely on this being a
ulong
ErrorCode
[Target(OS(Windows))] type ErrorCode = uint
The raw platform error code carried by IoError.code: a Windows
DWORD error code from GetLastError(), such as ERROR_FILE_NOT_FOUND
(2). On POSIX this is an errno value instead.
The numbers differ between platforms, so portable code classifies an
error with IoError.kind() or its predicates (is_not_found(), …)
rather than comparing codes.
See also
IoError
Mode
[Target(OS(Windows))] type Mode = uint
File attribute bits as reported in Metadata.Permissions: on Windows the
whole FILE_ATTRIBUTE_* word of the file (FILE_ATTRIBUTE_READONLY is
1, FILE_ATTRIBUTE_DIRECTORY 16, FILE_ATTRIBUTE_REPARSE_POINT 1024,
…). On POSIX the same field holds mode_t permission bits instead.
See also
Metadata
Offset
type Offset = long
A byte position or distance within a file, as taken and returned by the
File.seek* methods: a signed 64-bit integer on every target (off_t on
POSIX, LARGE_INTEGER on Windows), so files larger than 4 GiB work
everywhere.
ErrorKind
enum ErrorKind
The portable kind of an IoError, as IoError.kind() returns it: the
platform’s error code sorted into the cases a program usually handles.
Each case except Other has a matching IoError predicate
(NotFound and is_not_found(), …), whose documentation lists the
codes it covers on each platform. A code that fits none of them, such as
the EIO of a short write or the EBADF of a closed File, is Other.
See also
IoError.kind
Example
import io;
var r = io.File.open(io.Path("settings.txt"));
if (r.is_err()) {
match (r.error().kind()) {
NotFound => { print("no settings file"); }
PermissionDenied => { print("settings file is not readable"); }
else => { print(r.error().message); }
}
}
Cases
NotFound
NotFound
The file, directory or host name does not exist (is_not_found()).
PermissionDenied
PermissionDenied
Access was denied (is_permission_denied()).
AlreadyExists
AlreadyExists
The target already exists (is_already_exists()).
NotADirectory
NotADirectory
A directory was expected but something else was found
(is_not_a_directory()).
IsADirectory
IsADirectory
A file was expected but a directory was found (is_is_a_directory()).
DirectoryNotEmpty
DirectoryNotEmpty
A directory to remove still has entries (is_directory_not_empty()).
InvalidInput
InvalidInput
An argument or the data was invalid (is_invalid_input()).
Interrupted
Interrupted
The operation was interrupted before it finished (is_interrupted()).
ConnectionRefused
ConnectionRefused
Nothing accepted the connection (is_connection_refused()).
ConnectionReset
ConnectionReset
The peer reset, aborted or closed the connection
(is_connection_reset()).
AddressInUse
AddressInUse
Another socket already uses the address (is_address_in_use()).
AddressNotAvailable
AddressNotAvailable
The address cannot be used on this machine
(is_address_not_available()).
TimedOut
TimedOut
The operation timed out (is_timed_out()).
Cancelled
Cancelled
Cancelling the task ended the operation (is_cancelled()).
Other
Other
Any other code, including 0 (no error).
IoError
struct IoError
An I/O failure: the raw platform error code plus a human-readable message.
kind() sorts the code into an ErrorKind, and a predicate per kind
(is_not_found(), is_permission_denied(), …) tests for one of them;
each knows the matching codes on the current platform. Test with those
rather than comparing code against numbers. Failures that Pepper detects
itself (invalid UTF-8, an empty path, a short write, a closed File)
use the platform’s own code for the kind they are (EINVAL /
ERROR_INVALID_PARAMETER for invalid input, ENOENT /
ERROR_FILE_NOT_FOUND for an empty path, …), so the kinds hold
for them too. A short write uses EIO / ERROR_WRITE_FAULT and a closed
File uses EBADF / ERROR_INVALID_HANDLE; those are ErrorKind.Other.
See also
IoResultErrorKind
Example
import io;
var r = io.File.open(io.Path("settings.txt"));
if (r.is_err()) {
if (r.error().is_not_found()) {
print("no settings file");
} else {
print(r.error().message);
}
}
Fields
code
ErrorCode code
The raw platform error code: an errno value on POSIX, a
GetLastError() code on Windows; 0 means no error.
message
String message
A human-readable description: the platform’s text for code
(strerror on POSIX, FormatMessageW on Windows, without its
trailing period and line break), or Pepper’s own text such as
"empty path" for errors it detects itself.
Constructor
IoError()
construct(code: ErrorCode = 0, message: String = "")
Creates an error from a platform code and a message.
The default, IoError(), has code 0 and an empty message; no
predicate holds for it.
Parameters
code— the raw platform error code (errnoon POSIX,GetLastError()on Windows)message— the human-readable description
Static methods
last
static last(): IoError
Captures the calling thread’s last platform error now: errno on
POSIX, GetLastError() on Windows, with the platform’s message for
it.
Call it immediately after the failing system call; any call in between (including one that allocates) may overwrite the code.
Returns
- an error holding the current
errno/GetLastError()code and its message
invalid_input
static invalid_input(message: String): IoError
Makes an error for data or an argument that is not valid, such as a
damaged file a library parses, with the platform’s code for invalid
input, so is_invalid_input() holds.
Parameters
message— what is wrong
Returns
- the error
Example
import io;
var error = io.IoError.invalid_input("data.bin: bad header");
assert(error.is_invalid_input(), "the kind");
Methods
kind
kind(): ErrorKind
Returns the portable kind of the error: code sorted by the codes the
current platform uses for each kind. Each predicate below is
kind() == ErrorKind.X for its kind.
Returns
- the kind of
code;ErrorKind.Otherfor a code that fits no kind, and for 0
Example
import io;
var r = io.File.open(io.Path("missing.txt"));
if (r.is_err() && r.error().kind() == io.ErrorKind.NotFound) {
print("no such file");
}
is_not_found
is_not_found(): bool
Returns whether the error means the file or directory does not exist.
Matches ENOENT on POSIX, and ERROR_FILE_NOT_FOUND or
ERROR_PATH_NOT_FOUND on Windows. An empty path fails with this kind
on POSIX (with the message "empty path"), and in File.open_with
on every platform.
resolve and TcpStream.connect_host fail with this kind too when a
host name has no address.
Returns
- true when a path, or a directory on the way to it, is missing, or a host name has no address
is_permission_denied
is_permission_denied(): bool
Returns whether the operation was refused for lack of permission.
Matches EACCES or EPERM on POSIX, and ERROR_ACCESS_DENIED on
Windows (which Windows also reports for, among others, deleting a
read-only file).
Returns
- true when access was denied
is_already_exists
is_already_exists(): bool
Returns whether the operation failed because its target already
exists, such as Directory.create on an existing path or opening with
create_new.
Matches EEXIST on POSIX, and ERROR_FILE_EXISTS or
ERROR_ALREADY_EXISTS on Windows.
Returns
- true when the target already exists
is_not_a_directory
is_not_a_directory(): bool
Returns whether a path that had to name a directory names something
else, such as Directory.open or Directory.remove on a file (or,
on POSIX, a file used as a directory in the middle of a path).
Matches ENOTDIR on POSIX, and ERROR_DIRECTORY (267, “the directory
name is invalid”) on Windows.
Returns
- true when a directory was expected
is_is_a_directory
is_is_a_directory(): bool
Returns whether an operation that needs a file was given a directory,
such as reading from a directory opened as a File.
Matches EISDIR on POSIX, and ERROR_DIRECTORY_NOT_SUPPORTED on
Windows (which File reads report for a directory handle).
Returns
- true when a directory was given where a file was expected
is_directory_not_empty
is_directory_not_empty(): bool
Returns whether a directory could not be removed because it still has
entries (Directory.remove on a non-empty directory).
Matches ENOTEMPTY on POSIX (39 on Linux, 66 on macOS) and
ERROR_DIR_NOT_EMPTY on Windows.
Returns
- true when the directory is not empty
is_invalid_input
is_invalid_input(): bool
Returns whether an argument was invalid: bad options (such as opening with no access mode), data that is not valid UTF-8 where text was expected, or a malformed path.
Matches EINVAL on POSIX, and ERROR_INVALID_PARAMETER or
ERROR_INVALID_NAME on Windows.
Returns
- true when the input was invalid
is_interrupted
is_interrupted(): bool
Returns whether the operation was interrupted before it finished.
Matches EINTR on POSIX and WSAEINTR on Windows. An aborted
Windows operation (ERROR_OPERATION_ABORTED) is is_cancelled()
instead. The io functions retry interrupted POSIX opens, reads and writes
themselves, so callers rarely see this.
Returns
- true when the operation was interrupted
is_connection_refused
is_connection_refused(): bool
Returns whether a connection attempt was refused: nothing listens on that port (the peer answered with a reset).
Matches ECONNREFUSED on POSIX and WSAECONNREFUSED on Windows.
Returns
- true when the connection was refused
is_connection_reset
is_connection_reset(): bool
Returns whether an established connection was broken by the peer: reset, aborted, or (when writing) already closed.
Matches ECONNRESET, ECONNABORTED or EPIPE on POSIX, and
WSAECONNRESET or WSAECONNABORTED on Windows.
Returns
- true when the connection was reset or closed by the peer
is_address_in_use
is_address_in_use(): bool
Returns whether binding failed because another socket already uses the address and port.
Matches EADDRINUSE on POSIX and WSAEADDRINUSE on Windows.
Returns
- true when the address is in use
is_address_not_available
is_address_not_available(): bool
Returns whether an address cannot be used here: binding to an IP address that is not one of this machine’s, or connecting to port 0.
Matches EADDRNOTAVAIL on POSIX and WSAEADDRNOTAVAIL on Windows.
Returns
- true when the address is not available
is_timed_out
is_timed_out(): bool
Returns whether an operation gave up because the peer did not answer
in time, such as a connection attempt to an unreachable host, or
because an async.timeout scope around it expired.
Matches ETIMEDOUT on POSIX and WSAETIMEDOUT on Windows.
Returns
- true when the operation timed out
is_cancelled
is_cancelled(): bool
Returns whether an operation stopped because the task waiting for it
was cancelled (see async.Task.cancel).
Matches ECANCELED on POSIX, and ERROR_OPERATION_ABORTED or
ERROR_CANCELLED on Windows.
Returns
- true when cancellation ended the operation
IoResult
type IoResult<T> = Result<T, IoError>
The result of an io operation: a value on success, an IoError on
failure. Check is_ok() / is_err() before calling value() or error();
operations that produce nothing return IoResult<void>.
Type parameters
T— the value an operation produces on success
See also
IoError
Example
import io;
var cwd = io.Directory.current();
if (cwd.is_ok()) {
print(cwd.value().to_string());
} else {
print(cwd.error().message);
}
Reader and Writer
Reader and Writer: the traits of byte streams, such as a File, a
TcpStream, a UnixStream or a tls.TlsStream, so code can read from
or write to any of them.
Source: runtime/io/stream.pep
Reader
trait Reader
A stream of bytes that can be read: a File, TcpStream, UnixStream
or tls.TlsStream, or any type with read(buffer, max).
A type has the trait by providing read; it then gets lines() and
chunks(size), which read the stream as a for loop asks for more.
Take a Reader (or <R: Reader>) to accept any stream.
Example
import io;
count_lines(source: io.Reader): int {
var count = 0;
for (var line in source.lines()) {
if (line.is_err()) {
break;
}
count += 1;
}
return count;
}
Methods
read
read(buffer: u8[], max: ulong): IoResult<ulong>
Waits until some bytes are available, then appends up to max of
them to buffer.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to read
Returns
- the number of bytes appended, 0 at the end of the stream; an error when the stream fails
lines
lines(): Lines
Returns an iterator over the rest of the stream’s lines, read as it
goes, so a for loop handles a large file without reading it all.
Each element is an IoResult<String>: a line without its line ending
(\n, or \r\n). The last line counts even without a final line
ending, and a stream that ends with one has no empty line after it. A
read error, or a line that is not valid UTF-8 (is_invalid_input()),
is yielded as an error and ends the lines. The stream is read in
64 KiB chunks, so a file’s position after the loop is not the end of
the last line returned.
Returns
- a new
Linespositioned before the first line
Example
var f = File.open(Path("notes.txt")).value();
for (var line in f.lines()) {
print(line.value());
}
chunks
chunks(size: ulong): Chunks
Returns an iterator over the rest of the stream in chunks of at most
size bytes, each read as the loop asks for it.
Each element is an IoResult<u8[]>: a new array of what one read
returned, which may be fewer than size bytes (a socket returns what
has arrived). The iteration ends at the end of the stream; a read
error is yielded and ends it.
Parameters
size— the most bytes in one chunk
Returns
- a new
Chunkspositioned before the first chunk
Panics
- if
sizeis 0 (“Reader.chunks() needs a size above 0”)
Example
var total = 0;
for (var chunk in stream.chunks(4096)) {
total += chunk.value().count;
}
Writer
trait Writer
A stream of bytes that can be written: a File, TcpStream,
UnixStream or tls.TlsStream, or any type with write and
write_all.
A type has the trait by providing write and write_all(data: u8[]); it
then gets print and print_line, which write the text to_string makes
of a value (the text $"{value}" would hold).
Example
import io;
main(): int {
var path = io.Directory.temp().join("report.txt");
var file = io.File.create(path).value();
file.print("total: ");
file.print_line(42);
file.close();
assert(io.File.read_to_string(path).value() == "total: 42\n", "printed");
io.File.remove(path);
return 0;
}
Methods
write
write(data: u8[]): IoResult<ulong>
Writes what the stream takes in one call, which may be less than all
of data.
Parameters
data— the bytes to write
Returns
- the number of bytes written; an error when the stream fails
write_all
write_all(data: u8[]): IoResult<void>
Writes all of data.
Parameters
data— the bytes to write
Returns
- an Ok result once every byte is written; an error when the stream fails
print<T>(value: T): IoResult<void>
Writes the text of value, with no line break after it: a String as
it is, anything else through to_string.
Type parameters
T— the type ofvalue
Parameters
value— the value to print
Returns
- an Ok result once the text is written; an error as for
write_all
Example
file.print("n = ");
file.print(3); # the file now holds "n = 3"
print_line
print_line<T>(value: T): IoResult<void>
Writes the text of value and a line break (\n), as the global
print does on standard output.
Type parameters
T— the type ofvalue
Parameters
value— the value to print
Returns
- an Ok result once the line is written; an error as for
write_all
Example
file.print_line($"total: {total}");
file.print_line((1, "two")); # (1, two)
Lines
struct Lines
The iterator Reader.lines() returns: the lines of a stream, read in
chunks as the loop asks for them, each as an IoResult<String>.
See also
Reader.lines
Example
var f = File.open(Path("notes.txt")).value();
var it = f.lines();
while (it.next()) {
var line = it.current();
}
Constructor
Lines()
construct(source: Reader)
Makes an iterator over the lines of source, from what it reads next.
Parameters
source— the open stream to read
Methods
next
next(): bool
Reads the next line.
Once it returns false it keeps returning false.
Returns
- true if there is a current element (a line, or the error that ends the lines), false at the end of the stream
current
current(): IoResult<String>
Returns the element the last successful next() moved to.
Returns
- the current line, or the error that ended the lines
Panics
- if
next()was never called, or already returned false (“Lines.current() without a line”)
Chunks
struct Chunks
The iterator Reader.chunks(size) returns: the rest of a stream, one
read of at most size bytes at a time, each as an IoResult<u8[]>.
See also
Reader.chunks
Constructor
Chunks()
construct(source: Reader, size: ulong)
Makes an iterator over source in chunks of at most size bytes.
Parameters
source— the open stream to readsize— the most bytes in one chunk
Methods
next
next(): bool
Reads the next chunk.
Once it returns false it keeps returning false.
Returns
- true if there is a current element (a chunk, or the error that ends the chunks), false at the end of the stream
current
current(): IoResult<u8[]>
Returns the element the last successful next() moved to.
Returns
- the current chunk, or the error that ended the chunks
Panics
- if
next()was never called, or already returned false (“Chunks.current() without a chunk”)
Path
Source: runtime/io/path.pep
Path
struct Path
An owned file-system path: the text of the path plus operations on it
(Rust’s Path and PathBuf in one type).
Every method except exists, is_file, is_dir, metadata,
symlink_metadata and canonicalize is pure text manipulation: it never
touches the file system, so .. is not resolved against symlinks and
nothing needs to exist. A part that may be missing comes back as an
optional, as in Rust: parent() of a root is none.
Breaking change: parent(), file_name(), file_stem(), extension()
and dot_index() used to return an empty Path or String (or
name.size) for “none”; they now return Path?, String? and
ulong?.
Separators: on POSIX only / separates components (\ is an ordinary
byte of a name). On Windows both / and \ separate, and a path may
start with a prefix: a drive (C:) or a UNC share (\\server\share).
Methods that build a path (join, normalize, with_extension) insert
the platform separator (separator()), but the rest of the text is kept
as written. Paths are UTF-8 Strings, scanned byte by byte; separators and
dots are ASCII, so splitting at them never cuts a character in two.
Example
import io;
var p = io.Path("docs").join("guide.md"); # "docs/guide.md"
var name = p.file_name().value(); # "guide.md"
var ext = p.extension().value_or(""); # "md"
var dir = p.parent(); # some io.Path("docs")
if (p.is_file()) {
print(p.to_string());
}
Properties
is_empty
var is_empty: bool { get; }
Whether the path has no text at all. File-system operations on the empty path fail.
Constructor
Path()
[Implicit] construct(value: String = "")
Creates a path holding value as is: no validation, normalization or
separator conversion.
The constructor is [Implicit]: a String (or a string literal)
converts to a Path wherever a Path is expected, so every API that
takes a Path also accepts text.
Parameters
value— the path’s text; the default,"", is the empty path
Example
import io;
var p = io.Path("/usr/lib");
var empty = io.Path(); # is_empty is true
var q: io.Path = "docs"; # converted implicitly
Static methods
separator
static separator(): String
Returns the separator this platform inserts when it builds a path.
Returns
"/"on POSIX,"\"on Windows (where/is also accepted as a separator on input)
dot_index
static dot_index(name: String): ulong?
Returns the byte offset of the dot that starts the extension in the
file name name: the last . that is not the first byte. A leading
dot (.bashrc) starts the name, not an extension.
Breaking change: this used to return name.size for “no
extension”; it now returns none.
Parameters
name— a file name (one component, asfile_name()returns)
Returns
- the index of the extension’s dot, or none when the name has no such dot (
noext,.bashrc, the empty name)
Methods
to_string
to_string(): String
Returns the path’s text, the same String the Path holds, not a copy.
Returns
- the path as a String
is_absolute
is_absolute(): bool
Returns whether the path is absolute, meaning it does not depend on the current directory. Pure text: the path need not exist.
On POSIX a path is absolute when it starts with /. On Windows it is
absolute when it has a drive followed by a root separator (C:\x,
C:/x) or is a UNC path (\\server\share\x); a root without a drive
(\x) and a drive without a root (C:x) are both relative. The empty
path is relative.
Returns
- true when the path is absolute
is_relative
is_relative(): bool
Returns whether the path is relative: the opposite of
is_absolute(). The empty path is relative.
Returns
- true when the path is not absolute
join
join(other: Path): Path
Returns this path with other appended as further components.
A platform separator is inserted between the two unless this path
already ends in one. If other is absolute, or (on Windows) starts
with a root separator or a drive or UNC prefix, it replaces this path
instead, as in Rust. Joining an empty other returns a copy of this
path; joining onto the empty path returns other. Nothing is
normalized: a joined with ../b is a/../b.
Parameters
other— the path to append
Returns
- a new Path; this path is not changed
Example
import io;
var p = io.Path("usr").join(io.Path("lib")); # "usr/lib" ("usr\lib" on Windows)
var q = io.Path("a/").join(io.Path("b")); # "a/b"
var r = io.Path("a").join(io.Path("/abs")); # "/abs"
var so = io.Path("usr").join("lib").join("x.so"); # a String converts to a Path
join_str
[Deprecated("use 'join', which accepts a String", "join")] join_str(other: String): Path
Returns this path with the String other appended, exactly like
join(Path(other)). Deprecated: join accepts a String directly,
since a String converts to a Path implicitly.
Parameters
other— the path text to append
Returns
- a new Path; this path is not changed
See also
join
Example
import io;
var so = io.Path("usr").join("lib").join("x.so"); # "usr/lib/x.so"
has_root_or_prefix
has_root_or_prefix(): bool
Returns whether the path starts with a root separator or, on Windows,
a drive or UNC prefix. This is what makes join replace instead of
append; unlike is_absolute() it is also true for Windows paths such
as \x and C:x. On POSIX it equals is_absolute().
Returns
- true when the path has a root separator or a prefix
parent
parent(): Path?
Returns the path without its last component. Pure text: .. is not
resolved, so the parent of a/.. is a.
Trailing and repeated separators before the last component are
dropped (a/b/ and a//b both give a). The parent of a top-level
name keeps the root and prefix (/a gives /, C:\x gives C:\,
C:x gives C:).
Breaking change: this used to return the empty path for “no parent”; it now returns none.
Returns
- the parent path, or none for a root (
/,C:\, a bare prefix such asC:), a bare name with no root (a,.,..), or the empty path
Example
import io;
var p = io.Path("/a/b/c.txt").parent().value(); # "/a/b"
var root = io.Path("/a").parent().value(); # "/"
var none = io.Path("/").parent(); # none
file_name
file_name(): String?
Returns the last component of the path, ignoring trailing separators
(a/b/ gives b).
Breaking change: this used to return "" for “no file name”; it now
returns none.
Returns
- the last component, or none for a root (
/,C:\), the empty path, or a path whose last component is.or..
Example
import io;
var name = io.Path("/a/b/c.txt").file_name().value(); # "c.txt"
var none = io.Path("a/..").file_name(); # none
file_stem
file_stem(): String?
Returns the file name without its extension: everything before the last dot, unless that dot is the first character.
Breaking change: this used to return "" for “no file name”; it now
returns none.
Returns
- the stem (
c.tarforc.tar.gz,.bashrcfor.bashrc,fooforfoo.,noextfornoext), or none when there is no file name (seefile_name())
extension
extension(): String?
Returns the file name’s extension, without its dot: the text after
the last dot, unless that dot is the first character. Only the file
name counts; a dot in a directory name (a.b/c) is not an extension.
Breaking change: this used to return "" for “no extension”; it now
returns none. As in Rust, a name ending in a dot (foo.) has the
empty extension, which is not none.
Returns
- the extension (
gzforc.tar.gz,""forfoo.), or none when the file name has no dot after its first character (noext,.bashrc) or there is no file name
with_extension
with_extension(ext: String): Path
Returns this path with its file name’s extension replaced by ext,
added when it has none, or removed when ext is empty. Trailing
separators after the file name are dropped.
Parameters
ext— the new extension without a leading dot (a dot is inserted;".md"would giveb..md), or""to remove the extension
Returns
- the new path, or a copy of this path unchanged when it has no file name (a root,
.,.., or the empty path)
Example
import io;
var md = io.Path("a/b.txt").with_extension("md"); # "a/b.md"
var rs = io.Path("a/b").with_extension("rs"); # "a/b.rs"
var bare = io.Path("a/b.txt").with_extension(""); # "a/b"
components
components(): Array<String>
Returns the path split into its components: the prefix (Windows drive
or UNC share, such as C:), then the root separator (one character,
as written), then each non-empty part between separators.
Repeated and trailing separators produce no empty parts. . parts
are dropped, except a leading one in a path with no prefix or root
(./a gives . and a). .. parts are kept as they are.
Returns
- the components in order; empty for the empty path
Example
import io;
var c = io.Path("/usr//lib/./x/").components(); # ["/", "usr", "lib", "x"]
var d = io.Path("./a/b").components(); # [".", "a", "b"]
normalize
normalize(): Path
Returns the path cleaned up lexically, without touching the file
system: . parts and repeated or trailing separators are dropped, and
each .. removes the part before it.
A .. cannot climb above a root (/../x gives /x) and is kept at
the start of a relative path (../a/.. gives ..). The prefix is
kept, and every separator becomes the platform separator. Because
symlinks are not consulted, link/.. becomes . even when link
points elsewhere; use canonicalize() for the path the file system
resolves.
Returns
- the normalized path;
.when the path cleans to nothing (a/..,./), and the empty path for the empty path
See also
canonicalize
Example
import io;
var p = io.Path("a/./b/../c").normalize(); # "a/c" ("a\c" on Windows)
var q = io.Path("a/..").normalize(); # "."
exists
exists(): bool
Returns whether something exists at this path, following symlinks. A relative path is resolved against the current directory.
Any error counts as not existing: a dangling symlink, a path under a
directory that cannot be read, and the empty path all give false. Use
metadata() to tell those apart.
Returns
- true when the path (or a symlink’s target) can be stat’ed
is_file
is_file(): bool
Returns whether the path names a regular file, following symlinks.
Returns
- true for a regular file or a symlink to one; false for anything else, and on any error (including a missing path)
is_directory
is_directory(): bool
Returns whether the path names a directory, following symlinks.
Returns
- true for a directory or a symlink to one; false for anything else, and on any error (including a missing path)
is_dir
[Deprecated("use 'is_directory' instead", "is_directory")] is_dir(): bool
Deprecated: use is_directory instead. Returns whether the path
names a directory, following symlinks.
Returns
- the same as
is_directory()
metadata
metadata(): IoResult<Metadata>
Returns the metadata of the file or directory at this path, following
symlinks: the result describes the target (so on POSIX is_symlink is
always false). stat / statx on POSIX; on Windows the path is
opened for attributes only.
Returns
- the
Metadataof the path (or of a symlink’s target); an error when the path cannot be stat’ed:is_not_found()when it (or a symlink’s target) is missing or the path is empty,is_permission_denied()when a directory on the way cannot be searched, and on POSIXis_not_a_directory()when a file is used as a directory in the middle of the path
See also
symlink_metadata
Example
import io;
var m = io.Path("data.bin").metadata();
if (m.is_ok()) {
var size = m.value().size;
}
symlink_metadata
symlink_metadata(): IoResult<Metadata>
Returns the metadata of the path itself without following a symlink
at the end of it, so for a symlink is_symlink is true and the rest
describes the link rather than its target. lstat / statx with
AT_SYMLINK_NOFOLLOW on POSIX; on Windows the path is opened with
FILE_FLAG_OPEN_REPARSE_POINT, and any reparse point (junctions
included) reports is_symlink, together with is_directory for a directory
link. For anything that is not a symlink it equals metadata().
Returns
- the
Metadataof the path itself; an error as formetadata(), except that a dangling symlink succeeds
See also
metadata
canonicalize
canonicalize(): IoResult<Path>
Returns the absolute path the file system resolves this path to:
relative to the current directory, with every symlink, . and ..
resolved. Unlike normalize(), the path must exist.
POSIX uses realpath. Windows opens the path and asks for its final
name; the result uses \, and the \\?\ prefix Windows reports is
removed (\\?\UNC\server\share becomes \\server\share).
Returns
- the canonical absolute path; an error when the path cannot be resolved:
is_not_found()when it (or a symlink’s target) is missing or the path is empty,is_permission_denied()when a directory on the way cannot be searched
See also
normalize
Example
import io;
var real = io.Path("a/../b.txt").canonicalize();
if (real.is_ok()) {
print(real.value().to_string());
}
read_link
read_link(): IoResult<Path>
Returns the target a symbolic link points to, as stored in the link
(readlink on POSIX; on Windows the reparse point’s target, junctions
included). Only the link itself is read: a relative target stays
relative to the directory holding the link, and the target need not
exist.
On Windows the \??\ prefix of an absolute target is removed
(\??\UNC\server\share becomes \\server\share).
Returns
- the link’s target; an error with
is_invalid_input()when the path is not a symlink,is_not_found()when it is missing or empty, or another platform error
See also
File.symlinkDirectory.symlink
Example
import io;
var target = io.Path("current").read_link();
if (target.is_ok()) {
print(target.value().to_string());
}
set_permissions
set_permissions(mode: Mode): IoResult<void>
Sets the permission bits of the file or directory at this path,
following symlinks (chmod on POSIX).
mode holds the raw platform bits, as Metadata.permissions reports
them: on POSIX the mode_t bits mode & 07777 (493, octal 755, for
rwxr-xr-x). A directory’s bits control listing it (r), creating and
removing entries in it (w) and reaching anything inside it (x). On
Windows mode is the FILE_ATTRIBUTE_* word, and only
FILE_ATTRIBUTE_READONLY (1) is applied. WASI has no permission
bits, and this fails. set_read_only and set_executable change one
property portably.
Parameters
mode— the new permission bits
Returns
- an Ok result once they are set; an error with
is_not_found()when the path is missing or empty,is_permission_denied()when the caller does not own it, or another platform error
See also
File.set_permissions
Example
import io;
io.Path("deploy.sh").set_permissions(493); # octal 755: rwxr-xr-x
set_read_only
set_read_only(read_only: bool): IoResult<void>
Makes the file or directory at this path read-only, or writable
again, following symlinks: the portable part of set_permissions.
On POSIX, read-only clears every write bit (octal 222), and writable
sets only the owner’s (octal 200), so nothing becomes writable by
group or others this way. On Windows it sets or clears
FILE_ATTRIBUTE_READONLY, which Windows ignores on a directory. Fails
on WASI, which has no permission bits.
Parameters
read_only— true to make it read-only, false to make it writable by its owner
Returns
- an Ok result once it is changed; an error as for
metadata()orset_permissions
See also
Metadata.read_only
Example
import io;
var config = io.Path("settings.toml");
config.set_read_only(true);
var ro = config.metadata().value().read_only; # true
set_executable
set_executable(executable: bool): IoResult<void>
Makes the file at this path executable, or not, following symlinks, without raw permission bits.
On POSIX, executable adds an execute bit for each class (owner, group,
others) that may read the file, as chmod +x does with the usual
umask; not executable clears all three. Windows has no execute bit
(an extension such as .exe decides) and WASI no permission bits, so
there this does nothing and succeeds.
Parameters
executable— true to make the file executable, false to stop it
Returns
- an Ok result once it is changed; an error as for
metadata()orset_permissions
Example
import io;
io.File.write_text(io.Path("hello.sh"), "#!/bin/sh\necho hello\n");
io.Path("hello.sh").set_executable(true);
set_modified
set_modified(time: long): IoResult<void>
Sets the modification time of the file or directory at this path,
following symlinks and leaving its access time alone (utimensat on
POSIX, SetFileTime on Windows, where the time is kept to 100 ns).
A directory’s modification time changes whenever an entry is added or removed, so set it after its contents are final.
Parameters
time— the new time, in nanoseconds since the Unix epoch, asMetadata.modifiedreports it
Returns
- an Ok result once the time is set; an error with
is_not_found()when the path is missing or empty,is_permission_denied()when the caller may not change it, or another platform error
See also
File.set_modified
Example
import io;
var source = io.Path("a.txt").metadata().value();
io.Path("b.txt").set_modified(source.modified);
set_accessed
set_accessed(time: long): IoResult<void>
Sets the access time of the file or directory at this path, following
symlinks and leaving its modification time alone (utimensat on
POSIX, SetFileTime on Windows, where the time is kept to 100 ns).
Parameters
time— the new time, in nanoseconds since the Unix epoch, asMetadata.accessedreports it
Returns
- an Ok result once the time is set; an error as for
set_modified
See also
File.set_accessed
operator ==
==(rhs: Path): bool
Returns whether two paths have exactly the same text. The comparison
is byte by byte, with no normalization and case-sensitive on every
platform: a/b differs from a//b, from a/b/, and (on Windows)
from a\b and A/b. Compare normalize() or canonicalize()
results to ignore spelling differences.
Parameters
rhs— the path to compare with
Returns
- true when both paths’ texts are equal
operator !=
!=(rhs: Path): bool
Returns whether two paths differ in their text: the opposite of ==,
with the same byte-by-byte comparison.
Parameters
rhs— the path to compare with
Returns
- true when the paths’ texts differ
File
Source: runtime/io/file.pep
OpenOptions
struct OpenOptions
Describes how File.open_with (or OpenOptions.open) opens a file:
which access it asks for, and whether the file is created or truncated.
A builder in the style of Rust’s std::fs::OpenOptions. A new
OpenOptions has every option off; each setter stores its flag and
returns this same OpenOptions, so calls chain. The rules, checked when
the file is opened:
- At least one of
read,writeorappendmust be set, or opening fails withis_invalid_input(). create,create_newandtruncateneedwriteorappend, or opening fails withis_invalid_input().appendimplies write access, and every write goes to the current end of the file whatever the position; it wins overwrite.createopens an existing file as it is (combine withtruncateto empty it) and creates a missing one.create_newcreates the file and fails withis_already_exists()when it already exists, atomically (O_EXCLon POSIX,CREATE_NEWon Windows);createis then irrelevant.truncateempties an existing file on open.- Without
createorcreate_new, a missing file fails withis_not_found().
On POSIX a created file gets mode 0666 (or the mode set) less the
process umask, and the descriptor is opened close-on-exec. On Windows every file is opened
sharing read, write and delete access with other openers.
See also
File.open_with
Example
var r = OpenOptions().read(true).write(true).create(true).open(Path("data.bin"));
if (r.is_err()) {
return 1;
}
var f = r.value();
Constructor
OpenOptions()
construct()
Creates an OpenOptions with every option off. Set at least one of
read, write or append before opening.
Methods
read
read(v: bool): OpenOptions
Sets whether the file is opened for reading.
Parameters
v— true to request read access
Returns
- this
OpenOptions, for chaining
write
write(v: bool): OpenOptions
Sets whether the file is opened for writing. Writes go to the current position and overwrite what is there.
Parameters
v— true to request write access
Returns
- this
OpenOptions, for chaining
append
append(v: bool): OpenOptions
Sets append mode: the file is opened for writing and every write
goes to the current end of the file, whatever the position (seeking
still moves the position for reads). Implies write access, and takes
precedence over write.
Parameters
v— true to open in append mode
Returns
- this
OpenOptions, for chaining
truncate
truncate(v: bool): OpenOptions
Sets whether an existing file is truncated to 0 bytes when it is
opened. Requires write or append.
Parameters
v— true to truncate an existing file
Returns
- this
OpenOptions, for chaining
create
create(v: bool): OpenOptions
Sets whether a missing file is created. An existing file is opened as
it is unless truncate is also set. Requires write or append.
Parameters
v— true to create the file when it is missing
Returns
- this
OpenOptions, for chaining
create_new
create_new(v: bool): OpenOptions
Sets whether the file must be newly created: opening fails with
is_already_exists() when the path already exists. The check and the
creation are one atomic step. Requires write or append.
Parameters
v— true to require that the file does not exist yet
Returns
- this
OpenOptions, for chaining
mode
mode(mode: Mode): OpenOptions
Sets the permission bits a file gets when this open creates it, so it
never exists with the default mode: on POSIX the mode_t bits passed
to open, less the process umask, as Metadata.permissions reports
them (493, octal 755, for rwxr-xr-x). An existing file keeps its
mode. Ignored on Windows and WASI; use File.set_read_only there.
Parameters
mode— the permission bits for a new file; 438 (octal 666) by default
Returns
- this
OpenOptions, for chaining
Example
var script = OpenOptions().write(true).create_new(true).mode(493).open(Path("run.sh"));
open
open(path: Path): IoResult<File>
Opens path with these options. The same as
File.open_with(path, this).
Parameters
path— the file to open
Returns
- the open
File, positioned at the start; an error when the options break a rule (is_invalid_input()), the path is empty or missing (is_not_found()), it exists undercreate_new(is_already_exists()), or the system refuses (is_permission_denied()and others)
See also
File.open_with
Example
var log = OpenOptions().append(true).create(true).open(Path("log.txt")).value();
log.write_all("started");
FileType
enum FileType
The kind of a file system object, as Metadata.file_type and
DirEntry.file_type report it.
A symlink is reported as Symlink only where links are not followed
(Path.symlink_metadata() and directory listings); on Windows that is any
reparse point, junctions included, even one that points to a directory.
See also
Metadata
Example
import io;
var m = io.Path("notes.txt").metadata();
if (m.is_ok()) {
match (m.value().file_type) {
File => { print("a file"); }
Directory => { print("a directory"); }
Symlink, Other => { print("something else"); }
}
}
Cases
File
File
A regular file. On Windows, anything that is neither a directory nor a reparse point.
Directory
Directory
A directory.
Symlink
Symlink
A symbolic link itself (on Windows, any reparse point).
Other
Other
Anything else, such as a FIFO, socket or device on POSIX.
SeekFrom
enum SeekFrom
Where File.seek measures its offset from, with the offset itself.
See also
File.seek
Example
var f = File.open(Path("data.bin")).value();
var length = f.seek(SeekFrom.End(0)).value(); # at the end
f.seek(SeekFrom.Start(4)); # 4 bytes in
var at = f.seek(SeekFrom.Current(-2)).value(); # 2
Cases
Start
Start(offset: Offset)
offset bytes from the start of the file.
Parameters
offset— the new position; negative is an error
Current
Current(offset: Offset)
offset bytes from the current position.
Parameters
offset— the distance to move; negative moves back
End
End(offset: Offset)
offset bytes from the end of the file.
Parameters
offset— the distance from the end; negative moves back from it
StdStream
enum StdStream
One of the process’s standard streams, as File.std_file takes it.
See also
File.std_file
Cases
Input
Input
Standard input: file descriptor 0 on POSIX, STD_INPUT_HANDLE on
Windows.
Output
Output
Standard output: file descriptor 1 on POSIX, STD_OUTPUT_HANDLE on
Windows.
Error
Error
Standard error: file descriptor 2 on POSIX, STD_ERROR_HANDLE on
Windows.
Metadata
struct Metadata
Describes a file system object: its kind, size, read-only flag, permission bits and timestamps, as a snapshot taken when it was queried.
Returned by File.metadata(), Path.metadata() (which follows
symlinks), Path.symlink_metadata() (which does not) and
DirEntry.metadata(). The timestamps are nanoseconds since the Unix
epoch (1970-01-01 00:00:00 UTC). On POSIX the metadata comes from
statx (Linux) or stat (macOS); on Windows from
GetFileInformationByHandle.
Example
var m = Path("notes.txt").metadata();
if (m.is_ok() && m.value().is_file) {
var bytes = m.value().size;
}
Fields
size
ulong size
The file’s length in bytes.
file_type
FileType file_type
The kind of object: File, Directory, Symlink or Other. It
agrees with is_file, is_directory and is_symlink, except that a
Windows reparse point to a directory is Symlink with both
is_directory and is_symlink set.
is_file
bool is_file
Whether this is a regular file. On Windows, anything that is neither a directory nor a reparse point (symlink) counts as a file.
is_directory
bool is_directory
Whether this is a directory.
is_symlink
bool is_symlink
Whether this is a symbolic link itself. Only metadata that does not
follow links (Path.symlink_metadata()) can report one. On Windows
this is any reparse point (FILE_ATTRIBUTE_REPARSE_POINT), which
includes junctions.
read_only
bool read_only
Whether the file is read-only: on POSIX, no write permission bit
(0222) is set for anyone; on Windows, FILE_ATTRIBUTE_READONLY is set.
modified
long modified
The time of the last modification of the contents, in nanoseconds since the Unix epoch.
accessed
long accessed
The time of the last access, in nanoseconds since the Unix epoch (file systems mounted with noatime or relatime update it rarely or never).
created
long? created
The creation (birth) time, in nanoseconds since the Unix epoch; none where the platform does not report one, such as on Linux file systems that do not record it.
permissions
Mode permissions
The raw permission bits: on POSIX the mode_t permission bits
(mode & 07777, for example 256 for owner read); on Windows the
FILE_ATTRIBUTE_* bits.
Constructor
Metadata()
construct()
Creates a Metadata with every number zero, every flag false,
file_type Other and created none. Metadata is normally obtained
from a query such as File.metadata().
File
[NotCopyable( "owns an operating-system handle and cannot be copied; send its path, and open the file again on the receiving thread" )] struct File
An open file, or a standard stream, read and written through its
platform handle (a file descriptor on POSIX, a HANDLE on Windows).
Open one with File.open (read-only), File.create (write-only,
created or truncated), or File.open_with / OpenOptions.open for
anything else. The static conveniences read_bytes, read_to_string,
write_bytes, write_text, copy, rename and remove work on a
whole file by path.
I/O is unbuffered: each read and write is one system call. Reads and
writes start at the current position and move it forward; the seek_*
methods move it explicitly. A write to a standard stream first flushes
the output print has buffered, so the two never come out of order.
Closing: the File’s destructor closes an owned handle when the last
reference to the File is released, and ignores any error. Call close()
to close earlier or to observe the error. The standard streams
(stdin(), stdout(), stderr()) are never closed by a File. Every
operation on a closed File fails with an error that none of the
IoError predicates identify (EBADF on POSIX, ERROR_INVALID_HANDLE
on Windows).
Errors are IoError values carrying the platform’s code; use its
predicates (is_not_found(), is_permission_denied(), …) to tell
kinds apart portably.
Example
var f = File.create(Path("out.txt")).value();
f.write_all("hello");
f.close();
var text = File.read_to_string(Path("out.txt")).value(); # "hello"
Fields
path
Path path
The path the File was opened with; <stdin>, <stdout> or
<stderr> for a standard stream, and empty for a File() never
opened.
Constructor
File()
construct()
Creates a closed File that owns nothing yet: is_open() is false and
every I/O method fails. Use File.open, File.create or
File.open_with to get an open one.
Static methods
open
static open(path: Path): IoResult<File>
Opens an existing file for reading only. The position starts at 0.
A directory can be opened this way too, but reading it fails with
is_is_a_directory().
Parameters
path— the file to open
Returns
- the open
File; an error withis_not_found()when the path is empty or does not exist,is_permission_denied()when access is refused, or another platform error
Example
var r = File.open(Path("config.txt"));
if (r.is_err() && r.error().is_not_found()) {
return 0;
}
var text = r.value().read_string();
create
static create(path: Path): IoResult<File>
Opens a file for writing only, creating it if it is missing and truncating it to 0 bytes if it exists.
On POSIX a created file gets mode 0666 less the process umask.
Parameters
path— the file to create or truncate
Returns
- the open
File, empty and positioned at 0; an error withis_not_found()when the path is empty or its parent directory does not exist,is_permission_denied()when access is refused, or another platform error (on POSIX,is_is_a_directory()when the path is a directory)
Example
var f = File.create(Path("report.txt")).value();
f.write_all("done");
open_with
static open_with(path: Path, options: OpenOptions): IoResult<File>
Opens path as options describe. OpenOptions.open is the same
call written as a builder.
The options are checked first, as in Rust: at least one of read, write or append is required, and create, create_new or truncate require write or append. The position starts at 0 (append mode still writes at the end).
Parameters
path— the file to openoptions— the access mode and creation behavior
Returns
- the open
File; an error withis_invalid_input()when no access mode is set or create, create_new or truncate is set without write or append;is_not_found()when the path is empty, or the file (or a parent directory) is missing and neither create nor create_new is set;is_already_exists()when create_new is set and the path exists;is_permission_denied()when access is refused; or another platform error
Example
var opts = OpenOptions().write(true).create_new(true);
var r = File.open_with(Path("lock"), opts);
if (r.is_err() && r.error().is_already_exists()) {
return 1;
}
std_file
static std_file(stream: StdStream, name: String): File
Returns a File for one of the process’s standard streams. The File
does not own the handle, so neither close() nor the destructor
closes the stream. stdin(), stdout() and stderr() are the usual
way to call this.
On Windows a process without a console or redirection may get a null handle, and the File is then not open.
Parameters
stream— the streamname— the display name stored in the File’spath, such as<stdout>
Returns
- a File on that stream, that does not close the stream when released
Example
var err = File.std_file(StdStream.Error, "<stderr>");
err.write_all("warning");
std_file
[Deprecated("use 'std_file' with a 'StdStream' instead")] static std_file(which: int, name: String): File
Deprecated: use std_file(StdStream, String) instead. Returns a File
for the standard stream numbered which, as std_file does for the
matching StdStream.
Parameters
which— the stream: 0 for standard input, 1 for standard output, 2 for standard errorname— the display name stored in the File’spath
Returns
- a File on that stream; a File that is not open for any other
which
stdin
static stdin(): File
Returns a File reading the process’s standard input. Each call returns a new File on the same stream; the stream is never closed by a File. A read returns 0 at end of input (on Windows, also when the writing end of a pipe has closed).
Returns
- a File on standard input, named
<stdin>
stdout
static stdout(): File
Returns a File writing to the process’s standard output. Each call
returns a new File on the same stream, and close() only marks that
File closed: the stream stays usable. Writes are unbuffered, and each
one first flushes what print has buffered, so output from print
and from this File comes out in program order.
Returns
- a File on standard output, named
<stdout>
Example
var out = File.stdout();
out.write_all("hello");
stderr
static stderr(): File
Returns a File writing to the process’s standard error. Each call
returns a new File on the same stream, and close() only marks that
File closed: the stream stays usable. Writes are unbuffered, and each
one first flushes what print has buffered.
Returns
- a File on standard error, named
<stderr>
adopt
static adopt(transfer: FileTransfer): IoResult<File>
Takes the handle out of transfer (see into_transfer) into a new
File on the calling thread, whose path is empty. Only the first
adopt of a token gets the handle.
Parameters
transfer— the token frominto_transfer
Returns
- the File; an error when the token is empty (already adopted, or made from a closed File)
read_bytes
static read_bytes(path: Path): IoResult<u8[]>
Reads the whole file at path into a byte array (Rust’s
fs::read).
Parameters
path— the file to read
Returns
- the file’s bytes; an error as for
File.open(such asis_not_found()) orread_all
Example
var r = File.read_bytes(Path("image.png"));
if (r.is_ok()) {
var n = r.value().count;
}
read_to_string
static read_to_string(path: Path): IoResult<String>
Reads the whole file at path as UTF-8 text (Rust’s
fs::read_to_string).
Parameters
path— the file to read
Returns
- the file’s text; an error with
is_invalid_input()when it is not valid UTF-8, or an error as forFile.open(such asis_not_found()) orread_all
Example
var r = File.read_to_string(Path("notes.txt"));
if (r.is_err()) {
return 1;
}
var text = r.value();
write_bytes
static write_bytes(path: Path, data: u8[]): IoResult<void>
Writes data as the whole contents of the file at path, creating
it or replacing what it held (Rust’s fs::write), then closes it.
Not atomic: the file is truncated first, so a failure part way leaves it truncated or partly written.
Parameters
path— the file to writedata— the new contents
Returns
- an Ok result once the data is written and the file closed; an error as for
File.create(such asis_not_found()for a missing parent directory),write_all, orclose
Example
var data = Array<u8>();
data.append(1 as u8);
File.write_bytes(Path("one.bin"), data);
write_text
static write_text(path: Path, s: String): IoResult<void>
Writes s as UTF-8 as the whole contents of the file at path,
creating it or replacing what it held, then closes it. No newline is
added.
Not atomic: the file is truncated first, so a failure part way leaves it truncated or partly written.
Parameters
path— the file to writes— the new contents
Returns
- an Ok result once the text is written and the file closed; an error as for
File.create(such asis_not_found()for a missing parent directory),write_all, orclose
Note
- this is the design proposal’s
write_to_string; a staticwritewould clash with the instancewrite
Example
var r = File.write_text(Path("hello.txt"), "hello, world");
if (r.is_err()) {
return 1;
}
copy
static copy(from: Path, to: Path): IoResult<ulong>
Copies the contents of the file from to to, creating to or
truncating it first, in 64 KiB chunks.
Only the contents are copied: to keeps its own permissions and
timestamps (a new file gets the defaults of File.create). A failure
part way leaves to partly written.
Parameters
from— the file to copyto— the destination file
Returns
- the number of bytes copied;
is_invalid_input()whenfromandtoare the same file (after resolving symlinks), which truncatingtowould empty before it was read; otherwise an error as forFile.openonfrom(such asis_not_found()),File.createonto, or a read, write or close failure
Example
var n = File.copy(Path("a.txt"), Path("b.txt")).value();
rename
static rename(from: Path, to: Path): IoResult<void>
Renames or moves from to to, replacing an existing file at to
(rename on POSIX, MoveFileExW with MOVEFILE_REPLACE_EXISTING on
Windows).
Works for directories too. Moving between file systems (volumes) fails rather than copying.
Parameters
from— the existing pathto— the new path
Returns
- an Ok result on success; an error with
is_not_found()whenfrom(or the parent ofto) does not exist or, on POSIX, either path is empty,is_permission_denied()when access is refused, or another platform error
Example
File.rename(Path("draft.txt"), Path("final.txt"));
symlink
static symlink(target: Path, link: Path): IoResult<void>
Creates a symbolic link at link that points to the file target
(symlink on POSIX, CreateSymbolicLinkW on Windows). Use
Directory.symlink for a link to a directory: Windows makes the two
kinds differently, and a file link to a directory does not open
there. POSIX makes the same link either way.
target is stored as given and need not exist; a relative target is
resolved against the directory that holds the link, not the current
directory. On Windows, making a symlink needs Developer Mode or an
administrator, and fails with is_permission_denied() otherwise.
Parameters
target— the path the link points tolink— where to create the link
Returns
- an Ok result once the link exists; an error with
is_already_exists()when something is already atlink,is_not_found()when its parent is missing or a path is empty,is_permission_denied(), or another platform error
See also
Path.read_link
Example
File.symlink(Path("app-1.2.so"), Path("lib/app.so"));
remove
static remove(path: Path): IoResult<void>
Deletes the file at path. A symlink is removed itself, not its
target. Directories are not removed: use Directory.remove or
Directory.remove_all.
Parameters
path— the file to delete
Returns
- an Ok result on success; an error with
is_not_found()when the path does not exist (on POSIX, also when it is empty); when it is a directory,is_is_a_directory()on Linux andis_permission_denied()on macOS and Windows;is_permission_denied()when access is refused (on Windows also for a read-only file); or another platform error
Example
var r = File.remove(Path("old.txt"));
if (r.is_err() && !r.error().is_not_found()) {
return 1;
}
Methods
is_open
is_open(): bool
Returns whether the File holds a valid handle: true after a
successful open, false after close() and for a File() never
opened.
Returns
- true while the File is open
check_open
check_open(): IoResult<void>
Checks that the File is open, for use before an operation.
Returns
- an Ok result when the File is open; an error with the message “file is closed” otherwise (code
EBADFon POSIX,ERROR_INVALID_HANDLEon Windows; noIoErrorpredicate holds)
read
read(buffer: u8[], max: ulong): IoResult<ulong>
Reads up to max bytes from the current position and appends them
to buf, growing it; the bytes already in buf are kept. Advances
the position by the count read.
One system call: the read may be partial, returning fewer than max
bytes before the end of the file (always so for pipes and terminals),
and a single call moves at most about 2 GiB on POSIX or 1 GiB on
Windows. Loop until it returns 0 to read everything, or use
read_all. On POSIX an interrupted read (EINTR) is retried. On
failure buffer keeps its previous contents.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to read; 0 reads nothing and returns 0
Returns
- the number of bytes appended, 0 at end of file; an error when the File is closed, or
is_is_a_directory()when it is a directory, or another platform error
Example
var buf = Array<u8>();
while (true) {
var r = f.read(buf, 4096);
if (r.is_err() || r.value() == 0) {
break;
}
}
read_all
read_all(): IoResult<u8[]>
Reads everything from the current position to the end of the file, in 64 KiB chunks, leaving the position at the end.
Returns
- a new array of the bytes read (empty at end of file); an error when the File is closed,
is_is_a_directory()when it is a directory, or another platform error, in which case the bytes read so far are discarded
read_string
read_string(): IoResult<String>
Reads everything from the current position to the end of the file,
as read_all does, and returns it as UTF-8 text.
The bytes must be strictly valid UTF-8 (RFC 3629: no overlong forms, surrogates, or code points past U+10FFFF). A byte-order mark is kept as part of the text.
Returns
- the text read (empty at end of file); an error with
is_invalid_input()when the bytes are not valid UTF-8, or any errorread_allreturns
write
write(data: u8[]): IoResult<ulong>
Writes bytes from data at the current position (or at the end, in
append mode) with a single system call, and advances the position.
The write may be partial: it can write fewer than data.count
bytes, and one call moves at most about 2 GiB on POSIX or 1 GiB on
Windows. Use write_all to write everything. On POSIX an interrupted
write (EINTR) is retried.
Parameters
data— the bytes to write
Returns
- the number of bytes written, which may be less than
data.count; an error when the File is closed or was not opened for writing, or another platform error
write_all
write_all(data: u8[]): IoResult<void>
Writes all of data, repeating the write until every byte is
written. Advances the position by data.count.
If an error occurs part of data may already have been written.
Parameters
data— the bytes to write
Returns
- an Ok result once every byte is written; an error when the File is closed or was not opened for writing, when a write makes no progress (“failed to write the whole buffer”, with code
EIOon POSIX orERROR_WRITE_FAULTon Windows), or another platform error
write_all
write_all(s: String): IoResult<void>
Writes all of the UTF-8 bytes of s, as write_all does for bytes.
No newline or terminator is added.
Parameters
s— the text to write
Returns
- an Ok result once every byte is written; an error as for
write_all(u8[])
Example
var f = File.create(Path("greeting.txt")).value();
var r = f.write_all("hello, world");
if (r.is_err()) {
return 1;
}
write_string
[Deprecated("use 'write_all' instead", "write_all")] write_string(s: String): IoResult<void>
Deprecated: use write_all instead. Writes all of the UTF-8 bytes of
s, exactly like write_all(s).
Parameters
s— the text to write
Returns
- an Ok result once every byte is written; an error as for
write_all
See also
write_all
seek
seek(from: SeekFrom): IoResult<Offset>
Moves the position as from says: to an offset from the start, from
the current position, or from the end of the file.
Seeking past the end is allowed; a later write there fills the gap with zeros. A position before the start of the file is an error. Seeking a stream that has no position (a pipe or a terminal) fails on POSIX and has platform-defined results on Windows.
Parameters
from— the origin and the offset from it
Returns
- the new position, in bytes from the start of the file; an error when the File is closed, the resulting position would be negative (on POSIX, with
is_invalid_input()), or the stream cannot seek
Example
var f = File.open(Path("data.bin")).value();
var length = f.seek(SeekFrom.End(0)).value();
f.seek(SeekFrom.Start(0));
seek_start
seek_start(pos: Offset): IoResult<Offset>
Moves the position to pos bytes from the start of the file, like
seek(SeekFrom.Start(pos)) without allocating the SeekFrom.
Seeking past the end is allowed; a later write there fills the gap with zeros.
Parameters
pos— the new position, in bytes from the start
Returns
- the new position, in bytes from the start of the file; an error as for
seek
See also
seek
seek_end
seek_end(delta: Offset): IoResult<Offset>
Moves the position to delta bytes from the end of the file, like
seek(SeekFrom.End(delta)). seek_end(0) moves to the end and
returns the file’s length.
Parameters
delta— the offset from the end of the file, in bytes; negative moves back from the end
Returns
- the new position, in bytes from the start of the file; an error as for
seek
See also
seek
seek_current
seek_current(delta: Offset): IoResult<Offset>
Moves the position by delta bytes from where it is, like
seek(SeekFrom.Current(delta)). seek_current(0) returns the
current position without moving it.
Parameters
delta— the offset from the current position, in bytes; negative moves back
Returns
- the new position, in bytes from the start of the file; an error as for
seek
See also
seek
seek
[Deprecated("use 'seek' with a 'SeekFrom' instead")] seek(offset: Offset, whence: int): IoResult<Offset>
Deprecated: use seek(SeekFrom) instead. Moves the position by
offset relative to the origin whence.
Parameters
offset— the offset in bytes from the origin; may be negativewhence— the origin: 0 the start of the file, 1 the current position, 2 the end of the file
Returns
- the new position, in bytes from the start of the file; an error as for
seek(SeekFrom), or whenwhenceis not 0, 1 or 2
set_length
set_length(size: ulong): IoResult<void>
Truncates or extends the file to exactly size bytes; bytes added
are zeros. The position is kept, even when it is now past the end.
The File must have been opened for writing.
Parameters
size— the new length, in bytes
Returns
- an Ok result on success; an error when the File is closed, was not opened for writing, or the system refuses the size
set_len
[Deprecated("use 'set_length' instead", "set_length")] set_len(size: ulong): IoResult<void>
Deprecated: use set_length instead. Truncates or extends the file to
exactly size bytes.
Parameters
size— the new length, in bytes
Returns
- the same as
set_length(size)
sync
sync(): IoResult<void>
Flushes the file’s contents and metadata to the storage device,
waiting until they are written (fsync on POSIX,
FlushFileBuffers on Windows). Writes are already unbuffered in the
process; this forces them out of the operating system’s cache.
On Windows the File must have been opened with write access.
Returns
- an Ok result once the data is durable; an error when the File is closed or the device reports a failure, or another platform error
lock
lock(): IoResult<void>
Takes an exclusive advisory lock on the whole file, waiting until no
other holder has a lock on it, shared or exclusive. Modeled on Rust’s
File::lock.
The lock belongs to this File: another File opened on the same path
(in this process or another) contends with it, and it is released by
unlock, by close, or when the File is destroyed. Taking a lock
while this File already holds one is platform-defined, as in Rust:
POSIX converts the lock, Windows may wait forever. The lock is
advisory on POSIX (flock(LOCK_EX)): it binds only those who also
lock. On Windows (LockFileEx) it also stops other handles from
reading or writing the file while held. The wait blocks the calling
thread, also inside a task. WASI has no file locks.
Returns
- an Ok result once the lock is held; an error when the File is closed, on WASI, or another platform error
See also
try_lockunlock
Example
var f = OpenOptions().write(true).create(true).open(Path(".pep/fetch.lock")).value();
if (f.lock().is_ok()) {
# ... work no other locker does at the same time ...
f.unlock();
}
lock_shared
lock_shared(): IoResult<void>
Takes a shared advisory lock on the whole file, waiting until no
other holder has an exclusive lock on it. Any number of Files can
hold a shared lock at once. Modeled on Rust’s File::lock_shared;
otherwise as lock (flock(LOCK_SH) on POSIX, LockFileEx without
LOCKFILE_EXCLUSIVE_LOCK on Windows).
Returns
- an Ok result once the lock is held; an error as for
lock
See also
lock
try_lock
try_lock(): IoResult<bool>
Takes an exclusive advisory lock on the whole file if no other
holder has a lock on it, without waiting. Modeled on Rust’s
File::try_lock; otherwise as lock.
Returns
- true when the lock is now held, false when another holder has a lock on the file (Rust’s
TryLockError::WouldBlock); an error as forlock
See also
lock
Example
var held = f.try_lock();
if (held.is_ok() && !held.value()) {
print("another process holds the lock\n");
}
try_lock_shared
try_lock_shared(): IoResult<bool>
Takes a shared advisory lock on the whole file if no other holder
has an exclusive lock on it, without waiting. Modeled on Rust’s
File::try_lock_shared; otherwise as lock_shared.
Returns
- true when the lock is now held, false when another holder has an exclusive lock on the file; an error as for
lock
See also
lock_shared
unlock
unlock(): IoResult<void>
Releases the lock this File holds, shared or exclusive. Releasing a
File that holds none is not an error. Closing or destroying the File
releases its lock too. Modeled on Rust’s File::unlock.
Returns
- an Ok result once the lock is released; an error when the File is closed, on WASI, or another platform error
See also
lock
metadata
metadata(): IoResult<Metadata>
Queries the metadata of the open file itself (the target, when it was opened through a symlink): size, kind, read-only flag, permission bits and timestamps.
Returns
- the file’s
Metadata; an error when the File is closed, or another platform error
Example
var size = f.metadata().value().size;
set_permissions
set_permissions(mode: Mode): IoResult<void>
Sets the permission bits of the open file (fchmod on POSIX).
mode holds the raw platform bits, as Metadata.permissions reports
them: on POSIX the mode_t bits mode & 07777 (493, octal 755, for
rwxr-xr-x). On Windows it is the FILE_ATTRIBUTE_* word, and only
FILE_ATTRIBUTE_READONLY (1) is applied; the File needs write access
there. WASI has no permission bits, and this fails. set_read_only
and set_executable change one property portably.
Parameters
mode— the new permission bits
Returns
- an Ok result once they are set; an error when the File is closed,
is_permission_denied()when the caller does not own the file (or on Windows the File was opened without write access), or another platform error
See also
Path.set_permissions
Example
var f = File.create(Path("run.sh")).value();
f.set_permissions(493); # octal 755: rwxr-xr-x
set_read_only
set_read_only(read_only: bool): IoResult<void>
Makes the open file read-only, or writable again: the portable part
of set_permissions.
On POSIX, read-only clears every write bit (octal 222), and writable
sets only the owner’s (octal 200), so a file never becomes writable
by group or others this way. On Windows it sets or clears
FILE_ATTRIBUTE_READONLY, and the File needs write access. Fails on
WASI, which has no permission bits.
Parameters
read_only— true to make the file read-only, false to make it writable by its owner
Returns
- an Ok result once the file is changed; an error as for
metadata()orset_permissions
See also
Metadata.read_only
set_executable
set_executable(executable: bool): IoResult<void>
Makes the open file executable, or not, without raw permission bits.
On POSIX, executable adds an execute bit for each class (owner, group,
others) that may read the file, as chmod +x does with the usual
umask; not executable clears all three. Windows has no execute bit
(an extension such as .exe decides) and WASI no permission bits, so
there this does nothing and succeeds.
Parameters
executable— true to make the file executable, false to stop it
Returns
- an Ok result once the file is changed; an error as for
metadata()orset_permissions
Example
var f = File.create(Path("run.sh")).value();
f.write_all("#!/bin/sh\necho hi\n");
f.set_executable(true);
set_modified
set_modified(time: long): IoResult<void>
Sets the open file’s modification time, leaving its access time
alone (futimens on POSIX, SetFileTime on Windows).
On Windows the File needs write access, and the time is kept to 100 ns. A later write to the file sets its modification time again, so call this after the last write.
Parameters
time— the new time, in nanoseconds since the Unix epoch, asMetadata.modifiedreports it
Returns
- an Ok result once the time is set; an error when the File is closed,
is_permission_denied()when the caller does not own the file (or on Windows the File was opened without write access), or another platform error
See also
Path.set_modified
set_accessed
set_accessed(time: long): IoResult<void>
Sets the open file’s access time, leaving its modification time
alone (futimens on POSIX, SetFileTime on Windows).
On Windows the File needs write access, and the time is kept to 100 ns.
Parameters
time— the new time, in nanoseconds since the Unix epoch, asMetadata.accessedreports it
Returns
- an Ok result once the time is set; an error as for
set_modified
See also
Path.set_accessed
close
close(): IoResult<void>
Closes the File now, to observe the error the system reports (for example a deferred write failure). Without it the destructor closes the handle and ignores errors.
Idempotent: closing a closed File succeeds. The File counts as
closed afterwards even when closing reports an error, so the handle
is never closed twice. A standard stream is only marked closed: the
stream stays open, and File.stdout() and the like still work.
Returns
- an Ok result on success or when already closed; the platform error when closing an owned handle fails
Example
var f = File.create(Path("out.bin")).value();
f.write_all("data");
if (f.close().is_err()) {
return 1;
}
into_transfer
into_transfer(): FileTransfer
Moves the file’s handle into a FileTransfer, which another thread
takes with File.adopt, and leaves this File closed. A File is not
thread-safe, so this is how a thread hands one to another, such as
the pipe from a child process’s standard output to the thread that
reads it while this one writes to the child.
Returns
- the token; for a closed File, one that
adoptfails on
Example
import async;
import [File, Path] from io;
var transfer = File.open(Path("notes.txt")).value().into_transfer();
var reader = async.Thread.spawn(() => {
var mine = File.adopt(transfer).value();
var text = mine.read_all();
});
reader.join();
FileTransfer
[Shared] struct FileTransfer
A file’s handle on its way from one thread to another: made by
File.into_transfer, taken by File.adopt. Unlike a File, it is
thread-safe, so a shared function (a body for async.spawn or
Thread.spawn) can capture it and a Channel can carry it. If no one
adopts it, the handle is closed when the last reference goes, unless it
is one the File did not own (a standard stream).
Constructor
FileTransfer()
construct(handle: FileHandle, owned: bool)
Wraps a file handle. File.into_transfer makes these; use it.
Parameters
handle— the handle, or an invalid one for an empty tokenowned— whether the File the token came from closed its handle
Directory
Source: runtime/io/directory.pep
DirEntry
struct DirEntry
One entry of a directory listing, as Directory.entries() returns it.
The kind flags come from the listing itself and describe the entry
without following symlinks. On POSIX a symlink has is_symlink set and
is_file and is_directory clear; on Windows a symlink or junction to a
directory has both is_directory and is_symlink set. Entries that are none of
these (a FIFO, socket or device on POSIX) have all three flags clear. Call
metadata() for the details of what the entry points to.
Example
import io;
var list = io.Directory.open(io.Path("src")).value().entries().value();
ulong i = 0;
while (i < list.count) {
if (list[i].is_file) {
print(list[i].Name);
}
i = i + 1;
}
Fields
path
Path path
The entry’s full path: the listed directory’s path joined with
name (relative when the directory’s path was relative).
name
String name
The entry’s own file name, without any directory part.
file_type
FileType file_type
The kind of the entry, without following symlinks: Symlink for a
symlink (on Windows, any reparse point, even one to a directory),
Other for anything that is not a file, directory or symlink.
is_file
bool is_file
Whether the entry is a regular file (never true for a symlink).
is_directory
bool is_directory
Whether the entry is a directory. On POSIX never true for a symlink; on Windows also true for a symlink or junction to a directory.
is_symlink
bool is_symlink
Whether the entry is a symlink (on Windows, any reparse point, junctions included), which is not followed.
Constructor
DirEntry()
construct()
Creates an empty entry: empty path and name, all kind flags
false and file_type Other. Entries are normally obtained from
Directory.entries().
Methods
metadata
metadata(): IoResult<Metadata>
Returns the metadata of the entry, following symlinks, exactly like
Path.metadata() on the entry’s path. The file system is queried
again now, so the result reflects any change since the listing was
read.
Returns
- the entry’s
Metadata(of the target, for a symlink); an error when it cannot be stat’ed, withis_not_found()when the entry was removed since the listing or is a dangling symlink
See also
Path.metadata
Directory
struct Directory
A directory on the file system, identified by its path, together with static functions that create, remove and inspect directories and the process’s current and temporary directories.
A Directory holds no operating-system handle: open only checks that
the path is a directory, and each call to entries() reads the listing
afresh.
Example
import io;
var dir = io.Directory.temp().join("scratch").join("logs");
if (io.Directory.create_all(dir).is_ok()) {
var d = io.Directory.open(dir).value();
var count = d.entries().value().count;
io.Directory.remove_all(io.Directory.temp().join("scratch"));
}
Fields
path
Path path
The directory’s path, as given to open.
Constructor
Directory()
construct()
Creates a Directory with an empty path, which names no directory.
Use Directory.open to get one for an existing directory.
Static methods
open
static open(path: Path): IoResult<Directory>
Checks that path exists and is a directory (following symlinks) and
returns a Directory for it. Nothing is kept open, so the directory
can still be renamed or removed afterwards.
Parameters
path— the directory’s path; a relative path is resolved against the current directory whenever theDirectoryis used
Returns
- the
Directory; an error whenpathcannot be stat’ed (is_not_found()for a missing or empty path,is_permission_denied()) or is not a directory (is_not_a_directory(), message"not a directory")
Example
import io;
var d = io.Directory.open(io.Path("src"));
if (d.is_err() && d.error().is_not_a_directory()) {
print("src is a file");
}
create
static create(path: Path, mode: Mode = 511): IoResult<void>
Creates one directory (mkdir on POSIX, with mode less the
umask; CreateDirectoryW on Windows). Its parent must already exist.
Giving the mode here means the directory never exists with another
one. mode is the POSIX mode_t permission bits (448, octal 700,
for a directory only its owner may use); it is ignored on Windows
and WASI.
Parameters
path— the directory to createmode— the permission bits on POSIX, less the umask; 511 (octal 777) by default
Returns
- an Ok result on success; an error when the directory cannot be created:
is_already_exists()when anything (a file too) is already atpath,is_not_found()when the parent is missing or the path is empty,is_permission_denied()when the parent is not writable
See also
create_all
Example
import io;
var keys = io.Directory.create(io.Path("keys"), mode = 448); # octal 700
create_all
static create_all(path: Path, mode: Mode = 511): IoResult<void>
Creates a directory and any missing parents, like mkdir -p.
Succeeds without doing anything when path is empty or already a
directory (or a symlink to one). Parents are found lexically with
Path.parent(); a relative path is created under the current
directory. It is safe against another process creating the same
directories at the same time.
mode applies to path itself when it is created, as mkdir -p -m
does: missing parents get the default, 511 (octal 777) less the
umask, so the new directory can always be made inside them. An
existing directory keeps its mode. Ignored on Windows and WASI.
Parameters
path— the directory to create, with its parentsmode— the permission bits ofpathon POSIX, less the umask; 511 (octal 777) by default
Returns
- an Ok result on success; an error when a directory cannot be created:
is_already_exists()when a file (not a directory) is atpathor at one of its parents,is_permission_denied(), and so on. Parents created before the failure are left in place
Example
import io;
var r = io.Directory.create_all(io.Path("build").join("out").join("logs"));
symlink
static symlink(target: Path, link: Path): IoResult<void>
Creates a symbolic link at link that points to the directory
target (symlink on POSIX; CreateSymbolicLinkW with
SYMBOLIC_LINK_FLAG_DIRECTORY on Windows). Use File.symlink for a
link to a file: Windows makes the two kinds differently. POSIX makes
the same link either way.
target is stored as given and need not exist; a relative target is
resolved against the directory that holds the link, not the current
directory. On Windows, making a symlink needs Developer Mode or an
administrator, and fails with is_permission_denied() otherwise.
Parameters
target— the directory the link points tolink— where to create the link
Returns
- an Ok result once the link exists; an error with
is_already_exists()when something is already atlink,is_not_found()when its parent is missing or a path is empty,is_permission_denied(), or another platform error
See also
Path.read_link
Example
import io;
io.Directory.symlink(io.Path("releases/1.2"), io.Path("current"));
remove
static remove(path: Path): IoResult<void>
Removes an empty directory (rmdir on POSIX, RemoveDirectoryW on
Windows).
Parameters
path— the directory to remove; it must be empty
Returns
- an Ok result on success; an error when it cannot be removed:
is_directory_not_empty()when it still has entries,is_not_found()when it is missing or the path is empty,is_not_a_directory()whenpathis a file,is_permission_denied()
See also
remove_all
remove_all
static remove_all(path: Path): IoResult<void>
Removes path and, if it is a directory, everything inside it,
recursively. Symlinks are removed, never followed, so nothing outside
the tree is touched. A file or symlink at path itself is simply
removed.
Stops at the first failure, leaving whatever was not yet removed; on
Windows a read-only file inside the tree is such a failure
(is_permission_denied()).
Parameters
path— the file, symlink or directory tree to remove
Returns
- an Ok result on success; an error when something cannot be removed:
is_not_found()whenpathdoes not exist (unlikerm -rf, a missing path is an error) or is empty,is_permission_denied(), and so on
See also
remove
Example
import io;
var tree = io.Directory.temp().join("my-scratch");
var r = io.Directory.remove_all(tree);
if (r.is_err() && !r.error().is_not_found()) {
print(r.error().message);
}
current
static current(): IoResult<Path>
Returns the process’s current directory, against which relative paths
are resolved (getcwd on POSIX, GetCurrentDirectoryW on Windows).
Returns
- the current directory as an absolute path; an error when it cannot be determined, such as
is_not_found()on POSIX when the directory has been removed
See also
set_current
set_current
static set_current(path: Path): IoResult<void>
Changes the process’s current directory (chdir on POSIX,
SetCurrentDirectoryW on Windows). This affects the whole process,
every thread included: relative paths used anywhere afterwards
resolve against the new directory.
Parameters
path— the new current directory; a relative path is resolved against the old one
Returns
- an Ok result on success; an error when the directory cannot be entered:
is_not_found()when it is missing or the path is empty,is_not_a_directory()when it is a file,is_permission_denied()
See also
current
temp
static temp(): Path
Returns the directory for temporary files. It is not created or checked, so it may not exist.
On POSIX it is $TMPDIR with any trailing / removed, or /tmp
when TMPDIR is unset or empty (/data/local/tmp on Android, which
has no /tmp; an app should use its cache directory instead). On Windows it is what
GetTempPathW reports (from TMP, TEMP, USERPROFILE, or the
Windows directory) with its trailing separator removed unless it is
a root, or . if that call fails.
Returns
- the temporary directory’s path, never an error
Example
import io;
var scratch = io.Directory.temp().join("my-tool");
Methods
entries
entries(): IoResult<DirEntry[]>
Reads the directory’s listing now, all at once: every entry except
. and .., in the order the operating system returns them (not
sorted). Uses opendir/readdir on POSIX and
FindFirstFileW/FindNextFileW on Windows. To read the entries one
at a time instead, loop over the Directory itself (see iterate).
Returns
- the entries, possibly none; an error when the directory cannot be read:
is_not_found()when it no longer exists (or, on POSIX,pathis empty),is_permission_denied(), or on POSIXis_not_a_directory()whenpathnow names a file
Example
import io;
var list = io.Directory.open(io.Path(".")).value().entries();
if (list.is_ok()) {
var n = list.value().count;
}
iterate
iterate(): DirectoryIterator
Returns an iterator that reads the directory’s listing one entry at a
time, as for (var entry in dir) does, without building an array.
Each element is an IoResult<DirEntry>. The listing is opened by the
first next(), so a directory that cannot be read yields one error
(the same errors as entries()) and then ends; an error while
reading also ends it. The entries come in the order the operating
system returns them, without . and ... The listing is closed
when it ends, or when the iterator is released, so leaving the loop
early with break or return closes it too.
Returns
- a new
DirectoryIteratorpositioned before the first entry
Example
import io;
var dir = io.Directory.open(io.Path(".")).value();
for (var entry in dir) {
if (entry.is_err()) {
print(entry.error().message);
break;
}
print(entry.value().name);
}
DirectoryIterator
struct DirectoryIterator
The iterator Directory.iterate() returns, and a for loop over a
Directory drives: the directory’s entries, read one at a time, each
as an IoResult<DirEntry>.
It holds the open listing (a DIR* on POSIX, a find handle on Windows)
from the first next() until the listing ends, an error ends it, or the
iterator is released.
See also
Directory.iterate
Example
import io;
var it = io.Directory.open(io.Path(".")).value().iterate();
while (it.next()) {
var entry = it.current();
}
Constructor
DirectoryIterator()
construct(path: Path)
Makes an iterator over the entries of the directory at path. Nothing
is opened until the first next().
Parameters
path— the directory to list
Methods
next
next(): bool
Reads the next entry, opening the listing on the first call.
Once it returns false it keeps returning false.
Returns
- true if there is a current element (an entry, or the error that ends the listing), false once the listing has ended
current
current(): IoResult<DirEntry>
Returns the element the last successful next() moved to.
Returns
- the current entry, or the error that ended the listing
Panics
- if
next()was never called, or already returned false (“DirectoryIterator.current() without an entry”)
Network addresses
Network addresses: IpAddress (IPv4 or IPv6), SocketAddress (an IP
address and a port), and resolve, which turns a host name into socket
addresses.
Source: runtime/io/address.pep
IpAddress
struct IpAddress
An IPv4 or IPv6 address.
Make one with IpAddress.v4, IpAddress.v6, IpAddress.parse, or one
of the well-known addresses (loopback_v4(), unspecified_v6(), …).
IpAddress() is the IPv4 unspecified address 0.0.0.0.
to_string() prints IPv4 in dotted decimal (127.0.0.1) and IPv6 in the
RFC 5952 form (::1, 2001:db8::8a2e:370:7334); an IPv4-mapped IPv6
address prints as ::ffff:1.2.3.4. An IPv4 address and the IPv6 address
that maps it are different values.
An IPv6 address can carry a scope id, the network interface a link-local
address such as fe80::1 belongs to. Its text form is fe80::1%eth0
(RFC 4007), and a socket address carries it to the system as
sin6_scope_id.
See also
SocketAddress
Example
import io;
var a = io.IpAddress.parse("192.168.1.10").value();
var b = io.IpAddress.v6(8193, 3512, 0, 0, 0, 0, 0, 1); # 2001:db8::1
print(a.is_v4()); # true
print(b.to_string()); # 2001:db8::1
Constructor
IpAddress()
construct()
Creates the IPv4 unspecified address, 0.0.0.0.
Static methods
v4
static v4(a: u8, b: u8, c: u8, d: u8): IpAddress
Creates an IPv4 address from its four bytes, a.b.c.d.
Parameters
a— the first byteb— the second bytec— the third byted— the fourth byte
Returns
- the address
a.b.c.d
Example
var home = IpAddress.v4(127, 0, 0, 1);
v6
static v6(a: u16, b: u16, c: u16, d: u16, e: u16, f: u16, g: u16, h: u16): IpAddress
Creates an IPv6 address from its eight 16-bit groups, a:b:c:d:e:f:g:h.
Parameters
a— the first groupb— the second groupc— the third groupd— the fourth groupe— the fifth groupf— the sixth groupg— the seventh grouph— the eighth group
Returns
- the address
Example
var loopback = IpAddress.v6(0, 0, 0, 0, 0, 0, 0, 1); # ::1
loopback_v4
static loopback_v4(): IpAddress
Returns the IPv4 loopback address, 127.0.0.1.
Returns
127.0.0.1
loopback_v6
static loopback_v6(): IpAddress
Returns the IPv6 loopback address, ::1.
Returns
::1
unspecified_v4
static unspecified_v4(): IpAddress
Returns the IPv4 unspecified address, 0.0.0.0. Binding to it listens
on every IPv4 interface.
Returns
0.0.0.0
unspecified_v6
static unspecified_v6(): IpAddress
Returns the IPv6 unspecified address, ::. Binding to it listens on
every IPv6 interface (and, where the system allows dual-stack sockets
by default, such as Linux, on every IPv4 interface too).
Returns
::
parse
static parse(text: String): IoResult<IpAddress>
Parses an IPv4 address in dotted decimal (192.168.0.1) or an IPv6
address in the RFC 4291 text form (::1, fe80::1:2,
::ffff:10.0.0.1), with an optional scope (fe80::1%eth0).
IPv4 needs exactly four decimal parts from 0 to 255 without leading
zeros. IPv6 takes up to eight groups of one to four hex digits (either
case), at most one ::, and an optional IPv4 address as the last 32
bits. Brackets and surrounding spaces are not accepted.
An IPv6 address may end in % and a scope: a decimal scope id
(%3), or the name of a network interface (%eth0), which is looked
up on this machine (so it fails where there is no such interface).
Parameters
text— the address to parse
Returns
- the address; an error with
is_invalid_input()whentextis not a valid IPv4 or IPv6 address or names an unknown interface
Example
var r = IpAddress.parse("10.0.0.300");
print(r.is_err()); # true: 300 is out of range
var scoped = IpAddress.parse("fe80::1%7").value();
print(scoped.scope_id()); # 7
Methods
is_v4
is_v4(): bool
Returns whether this is an IPv4 address.
Returns
- true for IPv4, false for IPv6
is_v6
is_v6(): bool
Returns whether this is an IPv6 address (including an IPv4-mapped one,
::ffff:a.b.c.d).
Returns
- true for IPv6, false for IPv4
scope_id
scope_id(): uint
Returns the scope id of an IPv6 address: the index of the network
interface it belongs to (the 3 or eth0 of fe80::1%3 or
fe80::1%eth0), or 0 when it has none. Always 0 for IPv4.
Returns
- the scope id, 0 for none
with_scope_id
with_scope_id(id: uint): IpAddress
Returns this address with scope id id, such as the index of the
interface a link-local address is reached through; 0 removes the
scope.
Parameters
id— the scope id, 0 for none
Returns
- a copy of this address with that scope id
Panics
- if this is an IPv4 address and
idis not 0
Example
var a = IpAddress.parse("fe80::1").value().with_scope_id(2);
print(a.to_string()); # fe80::1%eth0 where interface 2 is eth0
is_loopback
is_loopback(): bool
Returns whether this is a loopback address: 127.0.0.0/8 for IPv4,
::1 for IPv6.
Returns
- true for a loopback address
is_unspecified
is_unspecified(): bool
Returns whether this is the unspecified address, 0.0.0.0 or ::.
Returns
- true for the unspecified address
to_bytes
to_bytes(): u8[]
Returns the address’s bytes in network order: 4 for IPv4, 16 for IPv6.
Returns
- a new array of the address bytes
Example
var bytes = IpAddress.v4(10, 0, 0, 1).to_bytes(); # [10, 0, 0, 1]
to_string
to_string(): String
Returns the address as text: dotted decimal for IPv4, the RFC 5952
form for IPv6 (lowercase hex, leading zeros dropped, the longest run of
two or more zero groups written ::, ::ffff:a.b.c.d for an
IPv4-mapped address). A scope id follows as % and the name of its
interface (fe80::1%eth0), or the number when no interface on this
machine has that index (fe80::1%9).
Returns
- the text form, which
IpAddress.parsereads back
operator ==
==(other: IpAddress): bool
Returns whether two addresses are the same: same family, same bytes and same scope id. An IPv4 address never equals an IPv6 one, not even the IPv6 address that maps it.
Parameters
other— the address to compare with
Returns
- true when both are equal
operator !=
!=(other: IpAddress): bool
Returns whether two addresses differ.
Parameters
other— the address to compare with
Returns
- true when the family, any byte or the scope id differs
SocketAddress
struct SocketAddress
An IP address and a port: where a socket is bound, or where it connects or sends.
Make one with SocketAddress(address, port) or SocketAddress.parse,
or get a list from resolve. SocketAddress() is 0.0.0.0:0. The text
form is address:port for IPv4 and [address]:port for IPv6.
See also
IpAddressresolve
Example
import io;
var any = io.SocketAddress(io.IpAddress.unspecified_v4(), 8080);
var local = io.SocketAddress.parse("[::1]:443").value();
print(local.port()); # 443
Constructors
SocketAddress()
construct()
Creates 0.0.0.0:0, the IPv4 unspecified address with port 0.
SocketAddress()
construct(address: IpAddress, port: u16)
Creates a socket address from an IP address and a port.
Port 0 asks the system to choose a free port when binding; read the
one it chose with the socket’s local_address().
Parameters
address— the IP addressport— the port
Static methods
parse
static parse(text: String): IoResult<SocketAddress>
Parses a.b.c.d:port (IPv4) or [ipv6]:port (IPv6), with the address
syntax of IpAddress.parse (a scope goes inside the brackets,
[fe80::1%eth0]:80) and a decimal port from 0 to 65535.
Parameters
text— the socket address to parse
Returns
- the socket address; an error with
is_invalid_input()when the text is malformed
Example
var a = SocketAddress.parse("127.0.0.1:8080").value();
var b = SocketAddress.parse("[::1]:8080").value();
Methods
address
address(): IpAddress
Returns the IP address.
Returns
- the IP address part
port
port(): u16
Returns the port.
Returns
- the port, 0 to 65535
is_v4
is_v4(): bool
Returns whether the IP address is IPv4.
Returns
- true for an IPv4 socket address
is_v6
is_v6(): bool
Returns whether the IP address is IPv6.
Returns
- true for an IPv6 socket address
to_string
to_string(): String
Returns the text form: a.b.c.d:port for IPv4, [ipv6]:port for
IPv6.
Returns
- the text form, which
SocketAddress.parsereads back
operator ==
==(other: SocketAddress): bool
Returns whether two socket addresses have equal IP addresses and the same port.
Parameters
other— the socket address to compare with
Returns
- true when both are equal
operator !=
!=(other: SocketAddress): bool
Returns whether two socket addresses differ.
Parameters
other— the socket address to compare with
Returns
- true when the address or the port differs
resolve
resolve(host: String, port: u16): IoResult<SocketAddress[]>
Looks up the socket addresses of a host, with port attached to each.
A host that is already an IP address (as IpAddress.parse reads it) is
returned as is, without a lookup. Anything else goes to the system
resolver (getaddrinfo on POSIX, GetAddrInfoW on Windows), which
consults the hosts file and DNS and blocks until it answers. The addresses
come in the resolver’s order of preference, IPv4 and IPv6 mixed, each at
most once.
Parameters
host— a host name such as"example.com"or"localhost", or an IP addressport— the port to put in every result
Returns
- at least one address; an error with
is_not_found()when the name has no address,is_invalid_input()whenhostis empty, or another resolver error
Example
import io;
var r = io.resolve("localhost", 8080);
if (r.is_ok()) {
for (var address in r.value()) {
print(address.to_string()); # 127.0.0.1:8080, [::1]:8080, ...
}
}
Sockets
TCP and UDP sockets: TcpListener accepts connections, TcpStream is
one connection, UdpSocket sends and receives datagrams.
Every call blocks the calling thread until it completes (in a task, it
parks only the task), or until the socket’s read or write timeout
expires. Addresses are SocketAddress values; resolve turns a host
name into them.
Source: runtime/io/socket.pep
TcpListener
[NotCopyable("owns an operating-system socket and cannot be copied")] struct TcpListener
A TCP socket that listens for connections.
TcpListener.bind opens one on a local address; each accept() waits for
the next connection and returns it as a TcpStream together with the
peer’s address. Binding to port 0 lets the system choose a free port;
local_address() tells which.
On POSIX the listener sets SO_REUSEADDR, so a server can restart while
connections from its previous run linger in TIME_WAIT. An IPv6 listener
keeps the system’s default for accepting IPv4 too (on Linux and macOS it
does, on Windows it does not).
Closing: the destructor closes the socket when the last reference goes;
close() closes it earlier and reports errors. Every operation on a
closed listener fails with an error no IoError predicate identifies
(EBADF on POSIX, ERROR_INVALID_HANDLE on Windows).
See also
TcpStream
Example
import io;
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
print(listener.local_address().value().to_string()); # 127.0.0.1:<port>
while (true) {
var (stream, peer) = listener.accept().value();
stream.write_all("hello " + peer.to_string());
}
Constructor
TcpListener()
construct()
Creates a closed listener: is_open() is false and every operation
fails. Use TcpListener.bind to get an open one.
Static methods
bind
static bind(address: SocketAddress): IoResult<TcpListener>
Opens a TCP socket bound to address and starts listening on it.
Parameters
address— the local address to listen on: a specific interface (127.0.0.1:8080), every interface (0.0.0.0:8080,[::]:8080), or port 0 for any free port
Returns
- the listening socket; an error with
is_address_in_use()when another socket has the port,is_address_not_available()when the address is not one of this machine’s,is_permission_denied()for a privileged port, or another platform error
Methods
is_open
is_open(): bool
Returns whether the listener is open.
Returns
- false after
close()and for aTcpListener()never bound
accept
accept(): IoResult<(TcpStream, SocketAddress)>
Waits for the next incoming connection and returns it.
Returns
- the connected stream and the peer’s address; an error when the listener is closed, or another platform error
incoming
incoming(): Incoming
Returns an iterator that accepts connections one at a time, for a
for loop over incoming streams.
Each step waits in accept() and yields the connected stream, or the
error accept() returned; an error does not end the iteration, so a
server can log it and keep going, or break. The iteration ends once
the listener is closed. Use accept() instead when the peer’s address
is needed.
Returns
- a new
Incomingpositioned before the first connection
Example
import io;
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:8080").value()).value();
for (var connection in listener.incoming()) {
if (connection.is_err()) {
print(connection.error().message);
continue;
}
connection.value().write_all("hello");
}
local_address
local_address(): IoResult<SocketAddress>
Returns the address the listener is bound to, with the port the system chose when it was bound to port 0.
Returns
- the local address; an error when the listener is closed, or another platform error
close
close(): IoResult<void>
Closes the listener now. Closing a closed listener does nothing. Connections already accepted stay open.
Returns
- an Ok result on success or when already closed; the platform error when closing fails
Incoming
struct Incoming
The iterator TcpListener.incoming() returns: each connection the
listener accepts, as an IoResult<TcpStream>.
next() waits for the next connection; an accept error is yielded as an
item rather than ending the iteration. The iteration ends once the
listener is closed. The iterator holds a reference to the listener, so
the listener stays open while the iterator lives.
Constructor
Incoming()
construct(listener: TcpListener)
Makes an iterator over the connections listener accepts.
Parameters
listener— the listener to accept on
Methods
next
next(): bool
Waits for the next connection.
Returns
- true with the connection or the accept error as the current item; false once the listener is closed
current
current(): IoResult<TcpStream>
Returns the item the last successful next() moved to.
Returns
- the accepted stream, or the error accepting it
Panics
- if
next()was never called or returned false (“Incoming.current() without a connection”)
SharedTcpListener
[Shared] struct SharedTcpListener
A TCP listening socket that any thread can accept connections on: the
thread-safe counterpart of TcpListener, for servers that accept on every
worker of async.run at once (see async.spawn_on_each_worker). Each
connection goes to one of the threads waiting in accept, and the stream
it returns belongs to that thread.
A shared function may capture it. There is no close(), since another
thread may be waiting on it: the socket closes when the last reference
goes, which, for accept loops in tasks, is once async.run has cancelled
them (accept then fails and the loop returns).
See also
TcpListener
Example
import async;
import io;
main(): int {
async.run(() => {
var listener = io.SharedTcpListener.bind(io.SocketAddress.parse("0.0.0.0:7000").value()).value();
async.spawn_on_each_worker(() => {
while (true) {
var accepted = listener.accept();
if (accepted.is_err()) {
return;
}
var (stream, peer) = accepted.value();
stream.write_all("hello");
}
});
});
return 0;
}
Constructor
SharedTcpListener()
construct(handle: SocketHandle)
Wraps a listening socket. Use SharedTcpListener.bind.
Parameters
handle— the listening socket, or an invalid handle for none
Static methods
bind
static bind(address: SocketAddress): IoResult<SharedTcpListener>
Opens a TCP socket bound to address and starts listening on it, as
TcpListener.bind does.
Parameters
address— the local address to listen on (port 0 for any free port)
Returns
- the listening socket, or the errors
TcpListener.bindreturns
Methods
accept
accept(): IoResult<(TcpStream, SocketAddress)>
Waits for the next incoming connection and returns it, as
TcpListener.accept does. Several threads may wait at once; each
connection wakes one of them (or, on some systems, several, and the
rest keep waiting).
Returns
- the connected stream, owned by the calling thread, and the peer’s address; an error when the waiting task is cancelled, or another platform error
local_address
local_address(): IoResult<SocketAddress>
Returns the address the listener is bound to, with the port the system chose when it was bound to port 0.
Returns
- the local address, or a platform error
TcpStream
[NotCopyable("owns an operating-system socket and cannot be copied")] struct TcpStream
A TCP connection: a reliable, ordered stream of bytes in both directions.
Get one from TcpStream.connect (to an address), TcpStream.connect_host
(to a host name) or TcpListener.accept. read appends what has arrived
to a byte array and returns 0 once the peer has closed its side; write
sends bytes.
Each call is one system call and blocks until it can make progress: a
read returns as soon as some bytes are there, not when max bytes are,
and a write may send fewer bytes than given. read_exact and
write_all loop until done. Sending never raises SIGPIPE; writing to a
connection the peer has closed fails with is_connection_reset().
Closing: the destructor closes the socket when the last reference goes;
close() closes it earlier and reports errors. Every operation on a
closed stream fails with an error no IoError predicate identifies
(EBADF on POSIX, ERROR_INVALID_HANDLE on Windows).
See also
TcpListener
Example
import io;
var stream = io.TcpStream.connect_host("example.com", 80).value();
stream.write_all("HEAD / HTTP/1.0\r\n\r\n");
var reply = Array<u8>();
while (stream.read(reply, 4096).value() > 0) {
}
Constructor
TcpStream()
construct()
Creates a closed stream: is_open() is false and every operation
fails. Use TcpStream.connect or TcpListener.accept to get an open
one.
Static methods
connect
static connect(address: SocketAddress): IoResult<TcpStream>
Opens a connection to address, waiting until it is established.
Parameters
address— the address to connect to
Returns
- the connected stream; an error with
is_connection_refused()when nothing listens there,is_timed_out()when the peer does not answer, or another platform error
connect_timeout
static connect_timeout(address: SocketAddress, milliseconds: ulong): IoResult<TcpStream>
Opens a connection to address like connect, but gives up with
is_timed_out() when it is not established within milliseconds,
instead of after the system’s own limit (often minutes).
Parameters
address— the address to connect tomilliseconds— the longest wait, at most 4294967295 (about 49 days); 0 waits as long asconnectdoes
Returns
- the connected stream, with no read or write timeout; an error with
is_timed_out()when the time runs out,is_invalid_input()whenmillisecondsis too large, or an error as forconnect
Example
var r = TcpStream.connect_timeout(SocketAddress.parse("192.0.2.1:80").value(), 2000);
if (r.is_err() && r.error().is_timed_out()) {
print("no answer within 2 seconds");
}
connect_host
static connect_host(host: String, port: u16): IoResult<TcpStream>
Resolves host with resolve and connects to the first of its
addresses that accepts, trying them in order.
Parameters
host— a host name or an IP addressport— the port to connect to
Returns
- the connected stream; the error from
resolvewhen the name has no address, or the error from the last address tried
Example
var r = TcpStream.connect_host("localhost", 8080);
if (r.is_err() && r.error().is_connection_refused()) {
print("nothing is listening on 8080");
}
adopt
static adopt(transfer: SocketTransfer): IoResult<TcpStream>
Takes the connection out of transfer (see into_transfer) into a
new stream on the calling thread. Only the first adopt of a token
gets the connection.
Parameters
transfer— the token frominto_transfer
Returns
- the stream; an error when the token is empty (already adopted, or made from a closed stream)
Methods
is_open
is_open(): bool
Returns whether the stream is open.
Returns
- false after
close()and for aTcpStream()never connected
read
read(buffer: u8[], max: ulong): IoResult<ulong>
Waits until data arrives, then appends up to max bytes of it to
buffer with a single system call, like File.read.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to read; 0 reads nothing and returns 0
Returns
- the number of bytes appended, 0 once the peer has closed its side of the connection; an error with
is_connection_reset()when the connection was reset, an error when the stream is closed, or another platform error
Example
var buffer = Array<u8>();
var n = stream.read(buffer, 4096).value(); # buffer.count == n
read_exact
read_exact(buffer: u8[], count: ulong): IoResult<void>
Reads until exactly count more bytes are appended to buffer.
Parameters
buffer— the array the bytes are appended tocount— the number of bytes to read
Returns
- an Ok result once
countbytes are appended; an error when the peer closes the connection first (“connection closed after n of count bytes”, codeEIOon POSIX,ERROR_HANDLE_EOFon Windows, which no predicate matches), or an error as forread. The bytes read before an error stay inbuffer.
write
write(data: u8[]): IoResult<ulong>
Sends bytes from data with a single system call, waiting until the
system can take at least some of them.
The send may be partial: it can send fewer than data.count bytes.
Use write_all to send everything.
Parameters
data— the bytes to send
Returns
- the number of bytes sent, which may be less than
data.count; an error withis_connection_reset()when the peer has closed or reset the connection, an error when the stream is closed, or another platform error
write_all
write_all(data: u8[]): IoResult<void>
Sends all of data, repeating the send until every byte is sent.
If an error occurs part of data may already have been sent.
Parameters
data— the bytes to send
Returns
- an Ok result once every byte is sent; an error as for
write
write_all
write_all(text: String): IoResult<void>
Sends all of the UTF-8 bytes of text, as write_all(u8[]) does.
No newline or terminator is added.
Parameters
text— the text to send
Returns
- an Ok result once every byte is sent; an error as for
write
Example
stream.write_all("ping");
write_string
[Deprecated("use 'write_all' instead", "write_all")] write_string(text: String): IoResult<void>
Deprecated: use write_all instead. Sends all of the UTF-8 bytes of
text, exactly like write_all(text).
Parameters
text— the text to send
Returns
- an Ok result once every byte is sent; an error as for
write_all
See also
write_all
shutdown_write
shutdown_write(): IoResult<void>
Closes the sending side of the connection: the peer’s reads return 0 once it has read everything sent before. This side can still read.
Returns
- an Ok result on success; an error when the stream is closed, or another platform error
set_no_delay
set_no_delay(enabled: bool): IoResult<void>
Turns Nagle’s algorithm off (true) or on (false) with
TCP_NODELAY. With it off, small writes go out at once instead of
being held back to combine with later ones: lower latency for
request/response traffic, more packets.
Parameters
enabled— true to send small writes immediately
Returns
- an Ok result on success; an error when the stream is closed, or another platform error
set_read_timeout
set_read_timeout(milliseconds: ulong): IoResult<void>
Limits how long a read waits: when no data arrives for
milliseconds, the read fails with is_timed_out() and the stream
stays open for another try. Each wait counts on its own, so a
read_exact may take longer in all. 0, the default, waits as long as
it takes. It works outside a task and in one, where the task’s
async.timeout can end the wait sooner.
Parameters
milliseconds— the longest wait, at most 4294967295 (about 49 days); 0 for none
Returns
- an Ok result; an error with
is_invalid_input()whenmillisecondsis too large, or an error when the stream is closed
Example
stream.set_read_timeout(5000); # give up after 5 seconds of silence
read_timeout
read_timeout(): ulong
Returns the read timeout set_read_timeout set.
Returns
- the timeout in milliseconds, 0 for none
set_write_timeout
set_write_timeout(milliseconds: ulong): IoResult<void>
Limits how long a write waits for the system to take more data: when
it can take none for milliseconds (the peer is not reading), the
write fails with is_timed_out(), possibly after sending part of the
data. Each wait counts on its own. 0, the default, waits as long as
it takes.
Parameters
milliseconds— the longest wait, at most 4294967295 (about 49 days); 0 for none
Returns
- an Ok result; an error with
is_invalid_input()whenmillisecondsis too large, or an error when the stream is closed
write_timeout
write_timeout(): ulong
Returns the write timeout set_write_timeout set.
Returns
- the timeout in milliseconds, 0 for none
local_address
local_address(): IoResult<SocketAddress>
Returns this side’s address of the connection.
Returns
- the local address; an error when the stream is closed, or another platform error
peer_address
peer_address(): IoResult<SocketAddress>
Returns the peer’s address.
Returns
- the peer’s address; an error when the stream is closed, or another platform error
into_transfer
into_transfer(): SocketTransfer
Moves the connection out of this stream, into a token another thread
can take it from with TcpStream.adopt. The stream is closed
afterwards (is_open() is false), and a task waiting on it on this
worker wakes with an error.
Inside async.run a socket belongs to the worker that uses it; this
is how a task hands a connection to a task on another worker, such as
one started with async.spawn.
Returns
- the token; for a closed stream, one that
adoptfails on
Example
import async;
import io;
serve(listener: io.TcpListener) {
var (stream, peer) = listener.accept().value();
var transfer = stream.into_transfer();
async.spawn<void>(() => {
var mine = io.TcpStream.adopt(transfer).value();
mine.write_all("hello from another worker");
});
}
close
close(): IoResult<void>
Closes the connection now. Closing a closed stream does nothing.
Returns
- an Ok result on success or when already closed; the platform error when closing fails
SocketTransfer
[Shared] struct SocketTransfer
A connection on its way from one thread to another: made by
TcpStream.into_transfer (or UnixStream.into_transfer), taken by
TcpStream.adopt (or UnixStream.adopt, for a Unix domain connection).
Unlike a stream, it is thread-safe, so a shared function (a body for
async.spawn or Thread.spawn) can capture it and a Channel can carry
it. If no one adopts it, the connection is closed when the last
reference goes.
Constructor
SocketTransfer()
construct(handle: SocketHandle)
Wraps a socket handle. TcpStream.into_transfer makes these; use it.
Parameters
handle— the socket, or an invalid handle for an empty token
UdpSocket
[NotCopyable("owns an operating-system socket and cannot be copied")] struct UdpSocket
A UDP socket: sends and receives datagrams, each delivered whole or not at all, in no guaranteed order.
UdpSocket.bind opens one on a local address (port 0 for any free port).
send_to and receive_from name the peer on each call; after connect,
send and receive use the connected peer and ignore datagrams from
anyone else.
Each receive takes one datagram. A datagram longer than max is
truncated to max bytes and the rest is lost, so size max for the
largest datagram expected (at most 65507 bytes over IPv4).
Closing: the destructor closes the socket when the last reference goes;
close() closes it earlier and reports errors.
Example
import io;
var socket = io.UdpSocket.bind(io.SocketAddress.parse("0.0.0.0:9000").value()).value();
var buffer = Array<u8>();
var (n, from) = socket.receive_from(buffer, 65507).value();
socket.send_to(buffer, from); # echo it back
Constructor
UdpSocket()
construct()
Creates a closed socket: is_open() is false and every operation
fails. Use UdpSocket.bind to get an open one.
Static methods
bind
static bind(address: SocketAddress): IoResult<UdpSocket>
Opens a UDP socket bound to address.
Parameters
address— the local address to receive on; port 0 for any free port
Returns
- the bound socket; an error with
is_address_in_use()when another socket has the port,is_address_not_available()when the address is not one of this machine’s, or another platform error
Methods
is_open
is_open(): bool
Returns whether the socket is open.
Returns
- false after
close()and for aUdpSocket()never bound
send_to
send_to(data: u8[], address: SocketAddress): IoResult<ulong>
Sends data as one datagram to address.
Parameters
data— the datagram’s bytesaddress— where to send it
Returns
- the number of bytes sent (all of
data); an error when the datagram is too large, when the socket is closed, or another platform error
receive_from
receive_from(buffer: u8[], max: ulong): IoResult<(ulong, SocketAddress)>
Waits for the next datagram and appends up to max of its bytes to
buffer.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to keep; the rest of a longer datagram is lost. 0 reads nothing and returns at once.
Returns
- the number of bytes appended and the sender’s address; an error when the socket is closed, or another platform error
connect
connect(address: SocketAddress): IoResult<void>
Sets the socket’s default peer: send goes to address, and
receive and receive_from only take datagrams from it. No packet is
sent.
Parameters
address— the peer
Returns
- an Ok result on success; an error when the socket is closed, or another platform error
send
send(data: u8[]): IoResult<ulong>
Sends data as one datagram to the connected peer.
Parameters
data— the datagram’s bytes
Returns
- the number of bytes sent; an error when the socket is not connected or is closed, or another platform error
receive
receive(buffer: u8[], max: ulong): IoResult<ulong>
Waits for the next datagram from the connected peer and appends up to
max of its bytes to buffer.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to keep; the rest of a longer datagram is lost. 0 reads nothing and returns at once.
Returns
- the number of bytes appended; an error when the socket is closed, or another platform error
set_broadcast
set_broadcast(enabled: bool): IoResult<void>
Allows (true) or forbids (false) sending to a broadcast address
such as 255.255.255.255 (SO_BROADCAST).
Parameters
enabled— true to allow broadcasts
Returns
- an Ok result on success; an error when the socket is closed, or another platform error
set_read_timeout
set_read_timeout(milliseconds: ulong): IoResult<void>
Limits how long a receive waits: when no datagram arrives for
milliseconds, receive or receive_from fails with
is_timed_out() and the socket stays open for another try. 0, the
default, waits as long as it takes. It works outside a task and in
one, where the task’s async.timeout can end the wait sooner.
Parameters
milliseconds— the longest wait, at most 4294967295 (about 49 days); 0 for none
Returns
- an Ok result; an error with
is_invalid_input()whenmillisecondsis too large, or an error when the socket is closed
Example
socket.set_read_timeout(5000); # give up after 5 seconds of silence
read_timeout
read_timeout(): ulong
Returns the read timeout set_read_timeout set.
Returns
- the timeout in milliseconds, 0 for none
set_write_timeout
set_write_timeout(milliseconds: ulong): IoResult<void>
Limits how long a send waits for room in the system’s buffer: when
there is none for milliseconds, the send fails with
is_timed_out(). 0, the default, waits as long as it takes.
Parameters
milliseconds— the longest wait, at most 4294967295 (about 49 days); 0 for none
Returns
- an Ok result; an error with
is_invalid_input()whenmillisecondsis too large, or an error when the socket is closed
write_timeout
write_timeout(): ulong
Returns the write timeout set_write_timeout set.
Returns
- the timeout in milliseconds, 0 for none
local_address
local_address(): IoResult<SocketAddress>
Returns the address the socket is bound to, with the port the system chose when it was bound to port 0.
Returns
- the local address; an error when the socket is closed, or another platform error
peer_address
peer_address(): IoResult<SocketAddress>
Returns the connected peer’s address.
Returns
- the peer’s address; an error when the socket is not connected or is closed, or another platform error
close
close(): IoResult<void>
Closes the socket now. Closing a closed socket does nothing.
Returns
- an Ok result on success or when already closed; the platform error when closing fails
Unix domain sockets
Unix domain sockets: UnixListener accepts connections on a path in the
file system, UnixStream is one connection, and UnixDatagram sends and
receives datagrams.
They work like TcpListener, TcpStream and UdpSocket, for processes
on the same machine: a path names the socket instead of an IP address and
a port. Linux, macOS and Windows (10 version 1803 and later) all have the
stream sockets; UnixDatagram is Linux and macOS only.
Binding creates a socket file at the path, and closing the listener does
not remove it: remove it with File.remove when the server is done, and
before binding again, since binding to a path that exists fails with
is_address_in_use().
Source: runtime/io/unix.pep
UnixListener
[NotCopyable("owns an operating-system socket and cannot be copied")] struct UnixListener
A Unix domain socket that listens for connections on a path.
UnixListener.bind creates the socket file and listens on it; each
accept() waits for the next connection and returns it as a
UnixStream.
Closing: the destructor closes the socket when the last reference goes;
close() closes it earlier and reports errors. Neither removes the socket
file. Every operation on a closed listener fails with an error no
IoError predicate identifies (EBADF on POSIX, ERROR_INVALID_HANDLE
on Windows).
See also
UnixStream
Example
import io;
var path = io.Directory.temp().join("server.sock");
io.File.remove(path); # left over from an earlier run
var listener = io.UnixListener.bind(path).value();
while (true) {
var stream = listener.accept().value();
stream.write_all("hello");
}
Constructor
UnixListener()
construct()
Creates a closed listener: is_open() is false and every operation
fails. Use UnixListener.bind to get an open one.
Static methods
bind
static bind(path: Path): IoResult<UnixListener>
Creates a Unix domain socket at path and starts listening on it.
Parameters
path— where to create the socket file: at most 107 bytes (103 on macOS)
Returns
- the listening socket; an error with
is_address_in_use()when a file already exists atpath,is_invalid_input()when the path is empty or too long,is_not_found()when its directory does not exist,is_permission_denied()when the directory cannot be written, or another platform error
Methods
is_open
is_open(): bool
Returns whether the listener is open.
Returns
- false after
close()and for aUnixListener()never bound
path
path(): Path
Returns the path the listener was bound to.
Returns
- the path given to
bind; an empty path for aUnixListener()never bound
accept
accept(): IoResult<UnixStream>
Waits for the next incoming connection and returns it.
Returns
- the connected stream; an error when the listener is closed, or another platform error
close
close(): IoResult<void>
Closes the listener now. Closing a closed listener does nothing. Connections already accepted stay open, and the socket file stays.
Returns
- an Ok result on success or when already closed; the platform error when closing fails
SharedUnixListener
[Shared] struct SharedUnixListener
A Unix domain listening socket that any thread can accept connections on:
the thread-safe counterpart of UnixListener, as SharedTcpListener is
of TcpListener. Each connection goes to one of the threads waiting in
accept, and the stream it returns belongs to that thread.
A shared function may capture it. There is no close(), since another
thread may be waiting on it: the socket closes when the last reference
goes. The socket file stays.
See also
UnixListener
Example
import async;
import io;
main(): int {
async.run(() => {
var listener = io.SharedUnixListener.bind(io.Path("/tmp/server.sock")).value();
async.spawn_on_each_worker(() => {
while (true) {
var accepted = listener.accept();
if (accepted.is_err()) {
return;
}
accepted.value().write_all("hello");
}
});
});
return 0;
}
Constructor
SharedUnixListener()
construct(handle: SocketHandle)
Wraps a listening socket. Use SharedUnixListener.bind.
Parameters
handle— the listening socket, or an invalid handle for none
Static methods
bind
static bind(path: Path): IoResult<SharedUnixListener>
Creates a Unix domain socket at path and starts listening on it, as
UnixListener.bind does.
Parameters
path— where to create the socket file
Returns
- the listening socket, or the errors
UnixListener.bindreturns
Methods
accept
accept(): IoResult<UnixStream>
Waits for the next incoming connection and returns it, as
UnixListener.accept does. Several threads may wait at once; each
connection wakes one of them (or, on some systems, several, and the
rest keep waiting).
Returns
- the connected stream, owned by the calling thread; an error when the waiting task is cancelled, or another platform error
UnixStream
[NotCopyable("owns an operating-system socket and cannot be copied")] struct UnixStream
A Unix domain stream connection: a reliable, ordered stream of bytes in both directions between two processes (or threads) on this machine.
Get one from UnixStream.connect or UnixListener.accept. Reading and
writing work exactly as on a TcpStream: read appends what has arrived
and returns 0 once the peer has closed its side, write may send part of
the data, and read_exact and write_all loop until done. Sending never
raises SIGPIPE; writing to a connection the peer has closed fails with
is_connection_reset() (or a broken-pipe error no predicate matches, on
some systems).
Closing: the destructor closes the socket when the last reference goes;
close() closes it earlier and reports errors. Every operation on a
closed stream fails with an error no IoError predicate identifies.
See also
UnixListener
Example
import io;
var stream = io.UnixStream.connect(io.Path("/tmp/server.sock")).value();
stream.write_all("ping");
var reply = Array<u8>();
stream.read_exact(reply, 4).value();
Constructor
UnixStream()
construct()
Creates a closed stream: is_open() is false and every operation
fails. Use UnixStream.connect or UnixListener.accept to get an
open one.
Static methods
connect
static connect(path: Path): IoResult<UnixStream>
Connects to the listener at path.
Parameters
path— the socket file aUnixListeneris bound to
Returns
- the connected stream; an error with
is_not_found()when no file exists atpath,is_connection_refused()when the file is there but nothing listens on it,is_invalid_input()when the path is empty or too long, or another platform error
adopt
static adopt(transfer: SocketTransfer): IoResult<UnixStream>
Takes the connection out of transfer (see into_transfer) into a
new stream on the calling thread. Only the first adopt of a token
gets the connection.
Parameters
transfer— the token frominto_transfer
Returns
- the stream; an error when the token is empty (already adopted, or made from a closed stream)
Methods
is_open
is_open(): bool
Returns whether the stream is open.
Returns
- false after
close()and for aUnixStream()never connected
read
read(buffer: u8[], max: ulong): IoResult<ulong>
Waits until data arrives, then appends up to max bytes of it to
buffer with a single system call, like TcpStream.read.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to read; 0 reads nothing and returns 0
Returns
- the number of bytes appended, 0 once the peer has closed its side of the connection; an error when the stream is closed, or another platform error
read_exact
read_exact(buffer: u8[], count: ulong): IoResult<void>
Reads until exactly count more bytes are appended to buffer.
Parameters
buffer— the array the bytes are appended tocount— the number of bytes to read
Returns
- an Ok result once
countbytes are appended; an error when the peer closes the connection first (“connection closed after n of count bytes”), or an error as forread. The bytes read before an error stay inbuffer.
write
write(data: u8[]): IoResult<ulong>
Sends bytes from data with a single system call, waiting until the
system can take at least some of them. The send may be partial; use
write_all to send everything.
Parameters
data— the bytes to send
Returns
- the number of bytes sent, which may be less than
data.count; an error when the peer has closed the connection, when the stream is closed, or another platform error
write_all
write_all(data: u8[]): IoResult<void>
Sends all of data, repeating the send until every byte is sent.
If an error occurs part of data may already have been sent.
Parameters
data— the bytes to send
Returns
- an Ok result once every byte is sent; an error as for
write
write_all
write_all(text: String): IoResult<void>
Sends all of the UTF-8 bytes of text, as write_all(u8[]) does.
No newline or terminator is added.
Parameters
text— the text to send
Returns
- an Ok result once every byte is sent; an error as for
write
Example
stream.write_all("ping");
write_string
[Deprecated("use 'write_all' instead", "write_all")] write_string(text: String): IoResult<void>
Deprecated: use write_all instead. Sends all of the UTF-8 bytes of
text, exactly like write_all(text).
Parameters
text— the text to send
Returns
- an Ok result once every byte is sent; an error as for
write_all
See also
write_all
shutdown_write
shutdown_write(): IoResult<void>
Closes the sending side of the connection: the peer’s reads return 0 once it has read everything sent before. This side can still read.
Returns
- an Ok result on success; an error when the stream is closed, or another platform error
set_read_timeout
set_read_timeout(milliseconds: ulong): IoResult<void>
Limits how long a read waits: when no data arrives for
milliseconds, the read fails with is_timed_out() and the stream
stays open for another try. Each wait counts on its own, so a
read_exact may take longer in all. 0, the default, waits as long as
it takes. It works outside a task and in one, where the task’s
async.timeout can end the wait sooner.
Parameters
milliseconds— the longest wait, at most 4294967295 (about 49 days); 0 for none
Returns
- an Ok result; an error with
is_invalid_input()whenmillisecondsis too large, or an error when the stream is closed
Example
stream.set_read_timeout(5000); # give up after 5 seconds of silence
read_timeout
read_timeout(): ulong
Returns the read timeout set_read_timeout set.
Returns
- the timeout in milliseconds, 0 for none
set_write_timeout
set_write_timeout(milliseconds: ulong): IoResult<void>
Limits how long a write waits for the system to take more data: when
it can take none for milliseconds (the peer is not reading), the
write fails with is_timed_out(), possibly after sending part of the
data. Each wait counts on its own. 0, the default, waits as long as
it takes.
Parameters
milliseconds— the longest wait, at most 4294967295 (about 49 days); 0 for none
Returns
- an Ok result; an error with
is_invalid_input()whenmillisecondsis too large, or an error when the stream is closed
write_timeout
write_timeout(): ulong
Returns the write timeout set_write_timeout set.
Returns
- the timeout in milliseconds, 0 for none
into_transfer
into_transfer(): SocketTransfer
Moves the connection out of this stream, into a token another thread
can take it from with UnixStream.adopt, as
TcpStream.into_transfer does. The stream is closed afterwards.
Returns
- the token; for a closed stream, one that
adoptfails on
close
close(): IoResult<void>
Closes the connection now. Closing a closed stream does nothing.
Returns
- an Ok result on success or when already closed; the platform error when closing fails
UnixDatagram
[Target(OS(Linux) || OS(Mac))] [NotCopyable("owns an operating-system socket and cannot be copied")] struct UnixDatagram
A Unix domain datagram socket (SOCK_DGRAM on AF_UNIX): like a
UdpSocket for processes on the same machine, with a path in place of
an address. Linux and macOS only; Windows has no Unix datagram sockets.
Each send_to delivers one whole datagram, in order, and the system
never drops one silently. While the receiver’s queue is full, a send
waits on Linux; on macOS it fails at once with the platform’s “No buffer
space available” (ENOBUFS) and sends nothing, so try again later.
UnixDatagram.bind creates a socket file at a path, which closing
does not remove (as for UnixListener); UnixDatagram.unbound makes a
socket that can send but has no path for replies; UnixDatagram.pair
makes two connected sockets.
Closing: the destructor closes the socket when the last reference goes;
close() closes it earlier and reports errors. Every operation on a
closed socket fails with an error no IoError predicate identifies
(EBADF).
See also
UdpSocketUnixStream
Example
import io;
var path = io.Directory.temp().join("events.sock");
io.File.remove(path); # left over from an earlier run
var server = io.UnixDatagram.bind(path).value();
var client = io.UnixDatagram.unbound().value();
client.send_to("ping".bytes(), path);
var buffer = Array<u8>();
var (n, from) = server.receive_from(buffer, 1024).value(); # 4, an empty path
Constructor
UnixDatagram()
construct()
Creates a closed socket: is_open() is false and every operation
fails. Use UnixDatagram.bind, unbound or pair to get an open
one.
Static methods
bind
static bind(path: Path): IoResult<UnixDatagram>
Creates a datagram socket at path, to receive datagrams sent there.
Parameters
path— where to create the socket file: at most 107 bytes (103 on macOS)
Returns
- the bound socket; an error with
is_address_in_use()when a file already exists atpath,is_invalid_input()when the path is empty or too long,is_not_found()when its directory does not exist,is_permission_denied()when the directory cannot be written, or another platform error
unbound
static unbound(): IoResult<UnixDatagram>
Creates a datagram socket with no path. It can send, but a receiver sees an empty path as the sender and cannot reply.
Returns
- the socket; an error when the system is out of descriptors, or another platform error
pair
static pair(): IoResult<(UnixDatagram, UnixDatagram)>
Creates two datagram sockets connected to each other, with no path
(socketpair(2)): what one sends with send, the other receives. A
common way to talk to a child thread or process.
Returns
- the two sockets; an error when the system is out of descriptors, or another platform error
Example
var (a, b) = io.UnixDatagram.pair().value();
a.send("hi".bytes());
var buffer = Array<u8>();
b.receive(buffer, 16); # buffer holds "hi"
Methods
is_open
is_open(): bool
Returns whether the socket is open.
Returns
- false after
close()and for aUnixDatagram()never opened
path
path(): Path
Returns the path the socket was bound to.
Returns
- the path given to
bind; an empty path for an unbound or paired socket
send_to
send_to(data: u8[], path: Path): IoResult<ulong>
Sends data as one datagram to the socket bound at path.
Parameters
data— the datagram’s bytespath— the socket file aUnixDatagramis bound to
Returns
- the number of bytes sent (all of
data); an error withis_not_found()when no socket file exists atpath,is_connection_refused()when nothing is bound to it any more,is_invalid_input()when the path is empty or too long, or another platform error (such as a datagram larger than the system allows)
receive_from
receive_from(buffer: u8[], max: ulong): IoResult<(ulong, Path)>
Waits for the next datagram and appends up to max of its bytes to
buffer.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to keep; the rest of a longer datagram is lost. 0 reads nothing and returns at once.
Returns
- the number of bytes appended and the sender’s path (empty when the sender is unbound); an error when the socket is closed, or another platform error
connect
connect(path: Path): IoResult<void>
Sets the socket’s default peer: send goes to the socket at path,
and receive and receive_from only take datagrams from it. No
datagram is sent.
Parameters
path— the socket file aUnixDatagramis bound to
Returns
- an Ok result on success; an error with
is_not_found()when no socket file exists atpath,is_connection_refused()when nothing is bound to it, or another platform error
send
send(data: u8[]): IoResult<ulong>
Sends data as one datagram to the connected peer (see connect and
pair).
Parameters
data— the datagram’s bytes
Returns
- the number of bytes sent; an error when the socket is not connected or is closed, or another platform error
receive
receive(buffer: u8[], max: ulong): IoResult<ulong>
Waits for the next datagram from the connected peer and appends up to
max of its bytes to buffer.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to keep; the rest of a longer datagram is lost. 0 reads nothing and returns at once.
Returns
- the number of bytes appended; an error when the socket is closed, or another platform error
set_read_timeout
set_read_timeout(milliseconds: ulong): IoResult<void>
Limits how long a receive waits: when no datagram arrives for
milliseconds, receive or receive_from fails with
is_timed_out() and the socket stays open for another try. 0, the
default, waits as long as it takes. It works outside a task and in
one, where the task’s async.timeout can end the wait sooner.
Parameters
milliseconds— the longest wait, at most 4294967295 (about 49 days); 0 for none
Returns
- an Ok result; an error with
is_invalid_input()whenmillisecondsis too large, or an error when the socket is closed
Example
socket.set_read_timeout(5000); # give up after 5 seconds of silence
read_timeout
read_timeout(): ulong
Returns the read timeout set_read_timeout set.
Returns
- the timeout in milliseconds, 0 for none
set_write_timeout
set_write_timeout(milliseconds: ulong): IoResult<void>
Limits how long a send waits for the receiver to take more: when its
queue stays full for milliseconds (it is not receiving), send or
send_to fails with is_timed_out() and sends nothing. 0, the
default, waits as long as it takes. On macOS a send to a full queue
fails at once instead (see UnixDatagram).
Parameters
milliseconds— the longest wait, at most 4294967295 (about 49 days); 0 for none
Returns
- an Ok result; an error with
is_invalid_input()whenmillisecondsis too large, or an error when the socket is closed
write_timeout
write_timeout(): ulong
Returns the write timeout set_write_timeout set.
Returns
- the timeout in milliseconds, 0 for none
close
close(): IoResult<void>
Closes the socket now. Closing a closed socket does nothing. The socket file stays.
Returns
- an Ok result on success or when already closed; the platform error when closing fails
Streams in memory
ByteReader and ByteWriter: a Reader over bytes already in memory,
and a Writer that collects what is written into memory, for code that
takes a stream when the data is in an array.
Source: runtime/io/memory.pep
ByteReader
struct ByteReader
A Reader over an array of bytes: each read takes the next bytes of
the array, and the stream ends after its last byte.
The reader keeps a reference to the array and does not copy it; changing the array while reading changes what is read.
Example
import io;
var data = "one\ntwo\n".bytes();
var count = 0;
for (var line in io.ByteReader(data).lines()) {
count += 1;
}
assert(count == 2, "two lines");
Properties
remaining
var remaining: ulong { get; }
The number of bytes not read yet.
Constructor
ByteReader()
construct(data: u8[])
Makes a reader positioned at the first byte of data.
Parameters
data— the bytes to read
Methods
read
read(buffer: u8[], max: ulong): IoResult<ulong>
Appends up to max of the next bytes to buffer.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to read
Returns
- the number of bytes appended, 0 once every byte has been read
ByteWriter
struct ByteWriter
A Writer that appends everything written to an array in memory,
bytes.
Example
import io;
var out = io.ByteWriter();
out.print("n = ");
out.print_line(3);
assert(out.bytes == "n = 3\n".bytes(), "collected");
Fields
bytes
var bytes: u8[]
The bytes written so far, in order.
Constructor
ByteWriter()
construct()
Makes a writer with nothing written yet.
Methods
write
write(data: u8[]): IoResult<ulong>
Appends all of data to bytes.
Parameters
data— the bytes to write
Returns
- the number of bytes written: always
data.count
write_all
write_all(data: u8[]): IoResult<void>
Appends all of data to bytes.
Parameters
data— the bytes to write
Returns
- an Ok result; writing to memory does not fail
Print, deprecated: Writer gives print and print_line to every
stream.
Source: runtime/io/print.pep
[Deprecated("use 'Writer' instead", "Writer")] trait Print
Deprecated: use Writer instead.
A byte stream that values can be printed to, as the global print prints
to standard output.
A type has the trait by providing write_all(data: u8[]); it then gets
print and print_line, which write the text to_string makes of a
value (the text $"{value}" would hold).
Example
import io;
main(): int {
var path = io.Directory.temp().join("report.txt");
var file = io.File.create(path).value();
file.print("total: ");
file.print_line(42);
file.close();
assert(io.File.read_to_string(path).value() == "total: 42\n", "printed");
io.File.remove(path);
return 0;
}
Methods
write_all
write_all(data: u8[]): IoResult<void>
Writes all of data.
Parameters
data— the bytes to write
Returns
- an Ok result once every byte is written; an error when the stream fails
print<T>(value: T): IoResult<void>
Writes the text of value, with no line break after it: a String as
it is, anything else through to_string.
Type parameters
T— the type ofvalue
Parameters
value— the value to print
Returns
- an Ok result once the text is written; an error as for
write_all
Example
file.print("n = ");
file.print(3); # the file now holds "n = 3"
print_line
print_line<T>(value: T): IoResult<void>
Writes the text of value and a line break (\n), as the global
print does on standard output.
Type parameters
T— the type ofvalue
Parameters
value— the value to print
Returns
- an Ok result once the line is written; an error as for
write_all
Example
file.print_line($"total: {total}");
file.print_line((1, "two")); # (1, two)
The async module
Threads and concurrency, modeled on Rust’s std::thread and C++
std::thread, and tasks: green threads that take turns on a few worker
threads.
Like io, async is a module: bring it in with import async (and write
async.Thread, async.logical_cpus()) or import names directly with
import [Thread, ThreadLocal] from async.
- Atomic:
Atomic<T>, a value threads read and update without a lock - Thread: starting, naming and joining threads,
ThreadIdandThreadBody - ThreadLocal: a variable with a separate value on every thread, and its
ThreadLocalGuard<T> - Processor counts:
logical_cpus()andphysical_cpus() - Channel:
Channel<T>, a queue that carries values between threads, and itsChannelIterator<T> - Mutex:
Mutex<T>, theMutexGuard<T>awithblock holds, andCondition<T> - Once:
Once, which runs a function once, andLazy<T>, a value built on first use - Tasks:
run,Task<T>,spawn,spawn_on_each_worker,run_blocking,sleep,yield_now,timeoutand cancellation, for many tasks taking turns on a few worker threads and parking on sockets - TaskLocal: a variable with a separate value in every task, and its
TaskLocalGuard<T> - I/O backends:
Backend, how a runtime waits for sockets
Only thread-safe values can be shared between threads: numbers, shared
function values (such as a thread’s body), and [Shared] structs such as
Atomic<T>, Thread, ThreadLocal<T>, Channel<T> and Mutex<T>. Any
other value crosses threads only as a new value that nothing else
references (Channel.send(move(x)), or send(copy(x))), or stays inside
a Mutex, whose with blocks the compiler keeps apart from the rest of
the program. Lazy<T> holds thread-safe
values only. See the language guide’s Threads chapter.
Example
import async;
const hits = async.Atomic<long>(0);
work() {
hits.fetch_add(1);
}
main(): int {
var workers = Array<async.Thread>();
var n = async.logical_cpus();
var i: ulong = 0;
while (i < n) {
workers.append(async.Thread.spawn(work));
i = i + 1;
}
for (var worker in workers) {
worker.join();
}
assert(hits.load() == n as long, "every worker ran");
return 0;
}
Source: runtime/async/module.pep
Atomic
Source: runtime/async/atomic.pep
Atomic
[Shared] struct Atomic<T: Shared>
A value of type T that threads can read and update without data races,
like C++ std::atomic<T>.
T may be a numeric type, a pointer, or a struct type. For a struct T
the Atomic holds a counted reference and every operation follows
std::shared_ptr rules: store and a successful compare_exchange acquire
the new reference and release the one they replace, and load hands the
caller its own reference. Memory ordering: load is an acquire,
store a release, and compare_exchange and the fetch_* operations are
acquire-release.
Atomic is a [Shared] struct, so copies of it share one slot and may
be used from several threads at once: pass the Atomic around (or keep it
in a const global) to share the value between threads, so T is
constrained Shared: a number, bool, an enum, a [Shared] struct, or
another thread-safe type. async.Atomic<String> is a compile error at
String.
Type parameters
T— the type of the stored value, which must be thread-safe (T: Shared)
Example
main(): int {
var counter = async.Atomic<long>(0);
var before = counter.fetch_add(5); # before == 0
if (counter.load() != 5) { return 1; }
return 0;
}
Constructor
Atomic()
construct(value: T = __default<T>())
Creates an Atomic holding value.
For a struct T the Atomic acquires a reference to value.
Parameters
value— the initial value; defaults to zero for a PODTand null for a structT
Example
var a = async.Atomic<int>(41);
var p = async.Atomic<Point>(); # holds null until a store
Methods
load
load(): T
Atomically reads the current value, with acquire ordering.
For a struct T the returned reference is acquired for the caller, so
it stays valid even if another thread stores a new value afterwards.
Returns
- the value currently stored
store
store(v: T)
Atomically replaces the stored value with v, with release ordering.
For a struct T this is a shared_ptr assignment: the slot acquires
v and releases its previous occupant, and the swap is a single atomic
exchange, so concurrent stores never leak or double-release.
Parameters
v— the new value
compare_exchange
compare_exchange(expected: T, desired: T): bool
Atomically replaces the stored value with desired if and only if it
currently equals expected (a compare-and-swap).
Equality is bitwise: a struct T compares references (the same object,
not equal contents), and a floating-point T compares bit patterns, so
0.0 and -0.0 differ. Unlike C++ compare_exchange_strong,
expected is not updated on failure; call load() to retry. On success
a struct slot acquires desired and releases the old occupant; on
failure no reference count changes.
Parameters
expected— the value the slot must hold for the exchange to happendesired— the value to store when it does
Returns
- true if the slot held
expectedand now holdsdesired, false if it was left unchanged
Example
var cur = counter.load();
while (!counter.compare_exchange(cur, cur + 1)) {
cur = counter.load();
}
exchange
exchange(v: T): T
Atomically replaces the stored value with v and returns the value it
replaced.
Implemented as a compare_exchange retry loop, so under heavy
contention it may retry several times. For a struct T the slot
acquires v, and the caller owns the returned reference.
Parameters
v— the new value
Returns
- the value stored immediately before the exchange
Example
var e = async.Atomic<int>(1);
var old = e.exchange(2); # old == 1, e.load() == 2
is_lock_free
is_lock_free(): bool
Reports whether this Atomic’s slot fits a native atomic instruction, so its operations never need a lock or an atomic library call.
True for every POD T of 8 bytes or less and for every struct T
(whose slot is one pointer). The answer depends only on T, never on
the stored value.
Returns
- true when the operations are lock-free
fetch_add
fetch_add(v: T): T
Atomically adds v to the stored value and returns the value before
the addition, like std::atomic<T>::fetch_add.
Integer T only. Overflow wraps around. For a non-integer T (a
floating-point or struct type) the call compiles but aborts the
program when it runs.
Parameters
v— the amount to add
Returns
- the value stored immediately before the addition
Example
var c = async.Atomic<long>(10);
var old = c.fetch_add(5); # old == 10, c.load() == 15
fetch_sub
fetch_sub(v: T): T
Atomically subtracts v from the stored value and returns the value
before the subtraction, like std::atomic<T>::fetch_sub.
Integer T only. Overflow wraps around. For a non-integer T the call
compiles but aborts the program when it runs.
Parameters
v— the amount to subtract
Returns
- the value stored immediately before the subtraction
fetch_and
fetch_and(v: T): T
Atomically replaces the stored value with its bitwise AND with v and
returns the value before the operation, like
std::atomic<T>::fetch_and.
Integer T only. For a non-integer T the call compiles but aborts
the program when it runs.
Parameters
v— the mask to AND with
Returns
- the value stored immediately before the operation
fetch_or
fetch_or(v: T): T
Atomically replaces the stored value with its bitwise OR with v and
returns the value before the operation, like
std::atomic<T>::fetch_or.
Integer T only. For a non-integer T the call compiles but aborts
the program when it runs.
Parameters
v— the bits to OR in
Returns
- the value stored immediately before the operation
fetch_xor
fetch_xor(v: T): T
Atomically replaces the stored value with its bitwise XOR with v and
returns the value before the operation, like
std::atomic<T>::fetch_xor.
Integer T only. For a non-integer T the call compiles but aborts
the program when it runs.
Parameters
v— the bits to XOR with
Returns
- the value stored immediately before the operation
Thread
Threads: starting, naming, identifying and joining operating-system threads.
Source: runtime/async/thread.pep
ThreadId
type ThreadId = ulong
Identifies a thread for as long as the program runs: every thread,
whether started with Thread.spawn or not, gets a distinct id the first
time it is asked for one, and ids are never reused.
Ids are assigned by the runtime in the order threads ask for them, starting
at 1; they are not the operating system’s thread ids. A ThreadId is a
number, so it can be compared, hashed and used as a HashMap key.
See also
Thread.id
ThreadBody
type ThreadBody = shared () => void
The type of a thread’s body: a function that takes no arguments and
returns nothing. Thread.spawn runs one on a new thread.
It is a shared function type, because the body runs on another thread:
a named function, or a closure that captures only thread-safe values
(numbers, Atomic<T>, [Shared] structs, other shared function
values). A closure capturing a String, say, is a compile error.
See also
Thread.spawn
Thread
[Shared] struct Thread
An operating-system thread.
Start one with Thread.spawn, which runs a function on a new thread and
returns its Thread, and wait for it to finish with join(). Every
thread, including the main thread, can get its own Thread with
Thread.current().
A Thread is [Shared]: the thread that started it and the thread itself
can both hold it. Dropping every reference without joining detaches the
thread: it keeps running, and its resources are freed when it finishes.
The body runs until it returns; there is no way to stop a thread from the
outside. A panic or failed assert on any thread ends the whole program.
Example
import async;
const total = Atomic<long>(0);
add_ten() {
total.fetch_add(10);
}
main(): int {
var a = async.Thread.spawn(add_ten, "adder a");
var b = async.Thread.spawn(add_ten, "adder b");
a.join();
b.join();
assert(total.load() == 20, "both threads ran");
return 0;
}
Constructor
Thread()
construct(name: String, body: ThreadBody)
Creates a Thread value for a thread that is already running. Programs
get threads from spawn and current() instead; a Thread made
directly is not attached to any thread and cannot be joined.
Parameters
name— the thread’s name; empty for an unnamed threadbody— the function the thread runs
Static methods
spawn
static spawn(body: ThreadBody, name: String = ""): Thread
Starts a new thread that runs body.
The new thread starts right away and runs until body returns. Its
Thread.current() is the Thread returned here.
Parameters
body— the function to run on the new threadname— a name for the thread, returned byname(); empty by default
Returns
- the new thread, to
join()or query
Panics
- if the operating system cannot create another thread
Example
var worker = Thread.spawn(do_work, "worker");
worker.join();
current
static current(): Thread
Returns the Thread of the thread that calls it.
For a thread started with spawn this is the Thread that spawn
returned. Any other thread (the main thread, or one started by C code)
gets a Thread the first time it asks and the same one afterwards; the
main thread’s is named main, the others are unnamed. Such a Thread
cannot be joined.
Returns
- the calling thread
Example
var me = Thread.current();
print(me.name(), "\n"); # "main" on the main thread
sleep
static sleep(milliseconds: ulong)
Pauses the calling thread for at least milliseconds milliseconds.
The operating system may sleep longer, but never shorter. In a task
(see run), it is async.sleep: the task waits and its worker thread
runs other tasks, and a cancel or the task’s timeout ends the wait
early.
Parameters
milliseconds— how long to sleep; 0 returns right away
Methods
id
id(): ThreadId
Returns the thread’s id, which no other thread in the program has.
Returns
- the thread’s id
name
name(): String
Returns the name the thread was given.
Returns
- the name passed to
spawn,mainfor the main thread, or an empty String for an unnamed thread
is_finished
is_finished(): bool
Reports whether the thread’s body has returned.
A thread that is not finished may finish at any moment, so a false
result can be out of date by the time it is used. Always false for a
Thread that was not started by spawn.
Returns
- true once the body has returned
is_joinable
is_joinable(): bool
Reports whether join() can be called: the thread was started by
spawn and has not been joined yet.
Returns
- true if the thread can be joined
join
join()
Waits for the thread’s body to return.
Returns right away if it already has. A thread can be joined once; a
Thread from current() for a thread not started by spawn cannot be
joined at all. Everything the thread did before it finished is visible
to the caller after join() returns.
Panics
- if the thread cannot be joined (see
is_joinable()), or if a thread tries to join itself
ThreadLocal
Per-thread values: ThreadLocal<T> gives every thread its own copy of a
variable.
Source: runtime/async/thread_local.pep
ThreadLocal
[Shared] struct ThreadLocal<T>
A variable with a separate value on every thread, like C++
thread_local or Rust’s thread_local!.
Each thread that calls set stores its own value. When a thread exits, its
value is released.
For a thread-safe T (a number, a [Shared] struct), get returns the
calling thread’s value (the default of T, zero or null, until that thread
sets one). For any other T, such as a String, the value is reached
through a with block, with (var value = local.enter()) { ... }, and
get is a compile error; set then takes a new value (a constructor call,
a string literal, copy(x)). Inside the block the value is kept apart
from the rest of the program, as for a Mutex.
A ThreadLocal is [Shared], so one object can be used from every
thread; it is usually a module-scope variable. T does not have to be
thread-safe, because each value stays on the thread that stored it.
Destroying a ThreadLocal releases the calling thread’s value; values
that other threads still hold are released when those threads exit.
A thread’s values are released when it exits if Thread.spawn started
it (the async runtime’s workers too). The main thread’s are released
only by clear(), and a thread started by C code leaks the values it did
not clear.
Type parameters
T— the type of the per-thread value
Example
import async;
const calls = async.ThreadLocal<int>();
count_call() {
calls.set(calls.get() + 1);
}
main(): int {
count_call();
count_call();
assert(calls.get() == 2, "two calls on this thread");
return 0;
}
Constructor
ThreadLocal()
construct()
Creates a thread-local variable. No thread has a value yet.
Methods
get
get(): T
Returns the calling thread’s value.
For a struct T the caller gets its own reference. Only for a
thread-safe T: for any other T, use with (var value = local.enter()).
Returns
- the value this thread last stored, or the default of
T(zero or null) if it has not stored one
set
set([Move] value: T)
Stores value as the calling thread’s value, replacing (and, for a
struct T, releasing) the one it held. Other threads’ values are not
affected.
For a T that is not thread-safe, value must be a new value (a
constructor call, a string literal, copy(x)), which the compiler
checks.
Parameters
value— the new value for this thread
enter
enter(): ThreadLocalGuard<T>
Returns the guard that a with block enters to reach the calling
thread’s value: with (var value = local.enter()) { ... }. The body
runs with the value bound; the else block runs when this thread has
no value (for a struct T).
Returns
- the guard; its
enter()returns this thread’s value
Example
const names = async.ThreadLocal<Array<String>>();
remember(String name) {
if (!names.is_set()) {
names.set(Array<String>());
}
with (var list = names.enter()) {
list.append(name.copy());
}
}
is_set
is_set(): bool
Reports whether the calling thread has stored a value since it started
or last called clear().
Returns
- true if this thread has a value
clear
clear()
Removes the calling thread’s value, releasing it for a struct T.
get() returns the default of T afterwards.
ThreadLocalGuard
[Isolated] struct ThreadLocalGuard<T>
The access to a thread’s ThreadLocal<T> value that a with block holds:
enter() returns the value. Get one from ThreadLocal.enter().
A guard is [Isolated]: it can only be the scope of a with header, and
the compiler checks the block with the confinement rule (see Mutex).
Type parameters
T— the type of the per-thread value
Constructor
ThreadLocalGuard()
construct(local: ThreadLocal<T>)
Creates the guard for the calling thread’s value of local; the same
as local.enter().
Parameters
local— the thread-local variable
Methods
enter
enter(): T
Returns the calling thread’s value, for the with header to bind.
Returns
- this thread’s value, or the default of
T(null for a struct) when it has none
exit
exit()
Does nothing: the value stays with the thread.
Mutex and Condition
Mutual exclusion: Mutex<T> owns a value that one thread at a time can
use, MutexGuard<T> is the lock a with block holds, and Condition<T>
lets a thread holding a Mutex sleep until another thread changes its value.
Source: runtime/async/mutex.pep
Mutex
[Shared] struct Mutex<T>
A value that one thread at a time can use, like Rust’s Mutex<T> or a C++
std::mutex paired with the data it protects.
The Mutex owns its value, and the way to reach it is a with block:
with (var value = mutex.lock()) { ... } waits until no other thread holds
the lock, binds the value, and unlocks when the block ends by any path
(falling off the end, return, break, continue). Everything a thread
did while holding the lock is visible to the next thread that takes it.
T is a struct type, such as Array<String> or a struct of your own. Inside
the with block the compiler keeps the locked value and the rest of the
program apart: a value that is not thread-safe (a String, an Array)
cannot move from one to the other, and no part of the locked value can be
returned from the block or stored outside it. Pass copy(x) to move a
value either way; new values (a constructor call, a string literal, a call
the compiler infers returns a fresh value) and thread-safe values such as
numbers pass freely. Mutex<int> is a compile error: use Atomic<int>.
A Mutex is [Shared]: keep it in a module-scope variable or pass it to
threads. It is not re-entrant: locking it again on the thread that holds
it panics instead of deadlocking. A panic while it is held ends the
program, so a Mutex is never left “poisoned”.
In a task (see run), waiting for the lock parks the task, not its worker
thread, so the other tasks there keep running, including one that holds
the lock. Each task counts as its own holder, and a task may hold the
lock across a wait of its own (a sleep, a socket call). A cancel does
not end the wait for the lock.
Type parameters
T— the protected value’s type, a struct type
Example
import async;
[Shared]
struct Account {
const checking: Atomic<long>;
const savings: Atomic<long>;
construct(long checking = 0, long savings = 0) {
this.checking = Atomic<long>(checking);
this.savings = Atomic<long>(savings);
}
}
const account = async.Mutex<Account>(Account(100, 0));
save(long amount) {
with (var a = account.lock()) {
# No other thread sees the money in neither account.
a.checking.fetch_sub(amount);
a.savings.fetch_add(amount);
}
}
Constructor
Mutex()
construct([Move] value: T)
Creates an unlocked Mutex that owns value.
value must be a new value, such as a constructor call, copy(x) or
move(x): nothing outside the Mutex may reference it. The caller hands
its reference over, so nothing is copied.
Parameters
value— the value to protect; a new value, which the compiler checks
Example
const names = async.Mutex<Array<String>>(Array<String>());
Methods
lock
lock(): MutexGuard<T>
Waits until the calling thread holds the lock, and returns the guard
that a with block enters to reach the value.
Use it as the scope of a with header: with (var v = mutex.lock()).
The lock is released when the guard is destroyed, which for a guard
created in the header is the end of the block. A guard kept anywhere
else holds the lock until its last reference is released.
Returns
- the guard; its
enter()returns the value, and destroying it unlocks
Panics
- if the calling thread already holds the lock
Example
with (var account = accounts.lock()) {
account.balance.fetch_add(10);
}
try_lock
try_lock(): MutexGuard<T>
Takes the lock only if no thread holds it right now, without waiting.
In with (var v = mutex.try_lock()) { ... } else { ... }, the body runs
when the lock was taken and the else block runs when it was not. The
calling thread already holding the lock counts as busy.
Returns
- the guard; its
enter()returns the value, or null when the lock was busy
Example
with (var account = accounts.try_lock()) {
account.balance.fetch_add(10);
} else {
print("busy; try later\n");
}
replace
replace([Move] value: T): T
Replaces the whole value and returns the one the Mutex held, which the caller then owns outright: nothing else references it any more.
Waits for the lock like lock(). value must be a new value, as for
the constructor, and is handed over without a copy.
Parameters
value— the new value; a new value, which the compiler checks
Returns
- the value the Mutex held
Panics
- if the calling thread already holds the lock
Example
var done = pending.replace(Array<Job>()); # take every job at once
is_held
is_held(): bool
Reports whether the caller holds the lock: the calling task, in a task, or else the calling thread.
Returns
- true between a
lock()(or a successfultry_lock()) by the caller and the guard’s destruction
condition
condition(): Condition<T>
Returns a new condition variable that waits on this Mutex.
Create it once and share it, as the Mutex is shared: every thread that waits or notifies must use the same Condition.
Returns
- a new Condition bound to this Mutex
Example
const queue = async.Mutex<Jobs>(Jobs());
const not_empty = queue.condition();
MutexGuard
[Isolated] struct MutexGuard<T>
The lock on a Mutex<T> that a with block holds: enter() returns the
Mutex’s value, and destroying the guard unlocks.
Get one from Mutex.lock() or Mutex.try_lock() in a with header. The
block releases the guard on every path out, so the lock cannot be left
held by mistake. A guard belongs to the thread that locked it; do not pass
it to another thread.
A guard is [Isolated]: it can only be the scope of a with header, so
the lock cannot be kept past the block, and the compiler checks the block
with the confinement rule (see Mutex).
Type parameters
T— the protected value’s type
Example
with (var totals = stats.lock()) {
totals.requests.fetch_add(1);
} # unlocked here
Fields
locked
bool locked
Whether the guard holds the lock: false once exit() has run, and for
a try_lock() that found the lock busy.
Constructor
MutexGuard()
construct(source: Mutex<T>, wait: bool = true)
Locks source and creates the guard that holds it; the same as
source.lock() (or source.try_lock() when wait is false).
Parameters
source— the Mutex to lockwait— true to wait for the lock; false to take it only if it is free right now
Panics
- if
waitis true and the calling thread already holds the lock
Methods
enter
enter(): T
Returns the Mutex’s value, for the with header to bind.
For a struct T the caller gets its own reference.
Returns
- the protected value, or the default of
T(null for a struct) when the guard does not hold the lock
exit
exit()
Unlocks the Mutex if the guard holds it. The destructor calls it; calling it again does nothing.
Condition
[Shared] struct Condition<T>
Lets threads that hold a Mutex sleep until another thread changes the
Mutex’s value, like C++ std::condition_variable. Get one from
Mutex.condition().
wait() must run while the calling thread holds the Mutex, normally inside
with (var v = mutex.lock()). It unlocks, sleeps until notified, and locks
again before returning. A waiter can also wake without a notification (a
spurious wakeup), so always wait in a loop that re-checks the value.
A thread that changes the value calls notify_one() or notify_all(),
usually while it still holds the lock.
A Condition is [Shared], like its Mutex.
In a task (see run), wait() parks the task, not its worker thread. A
cancel does not end it, since the caller’s loop would only wait again;
wait_for() still returns when its time runs out.
Type parameters
T— the Mutex’s value type
Example
import async;
[Shared]
struct Jobs {
const waiting: Atomic<long>;
construct() { waiting = Atomic<long>(0); }
}
const jobs = async.Mutex<Jobs>(Jobs());
const has_work = jobs.condition();
take_job() {
with (var j = jobs.lock()) {
while (j.waiting.load() == 0) {
has_work.wait();
}
j.waiting.fetch_sub(1);
}
}
add_job() {
with (var j = jobs.lock()) {
j.waiting.fetch_add(1);
has_work.notify_one();
}
}
Constructor
Condition()
construct(mutex: Mutex<T>)
Creates a condition variable that waits on mutex. mutex.condition()
does the same.
Parameters
mutex— the Mutex that waiters hold
Methods
wait
wait()
Unlocks the Mutex, sleeps until another thread calls notify_one() or
notify_all(), and locks the Mutex again before returning.
The thread may also wake without being notified, so call it in a loop that re-checks what it waits for.
Panics
- if the calling thread does not hold the Mutex
Example
with (var q = queue.lock()) {
while (q.waiting.load() == 0) {
not_empty.wait();
}
}
wait_for
wait_for(milliseconds: ulong): bool
Like wait(), but gives up after about milliseconds milliseconds.
The Mutex is locked again before it returns either way.
Parameters
milliseconds— the longest time to sleep
Returns
- true if the thread was woken (by a notification or spuriously), false if the time ran out
Panics
- if the calling thread does not hold the Mutex
wait_while
wait_while([Locked] predicate: (T) => bool)
Waits while predicate holds for the Mutex’s value: calls it with the
value, and while it returns true, waits as wait() does and asks
again. Returns once it returns false, with the Mutex still locked.
Wakeups without a notification are handled here: the predicate is asked again after every wake.
predicate is written at the call, as a closure: the compiler checks
its body as it checks a with block’s, with its parameter as the
locked value, so it cannot keep any part of the value or bring in a
value from outside.
Parameters
predicate— called with the locked value; true to keep waiting
Panics
- if the caller does not hold the Mutex
Example
with (var queue = jobs.lock()) {
not_empty.wait_while((queue: Array<Job>): bool => queue.count == 0);
var job = queue.remove_at(0);
}
wait_while_for
wait_while_for(milliseconds: ulong, [Locked] predicate: (T) => bool): bool
Like wait_while(), but gives up after about milliseconds
milliseconds. The Mutex is locked when it returns either way.
Parameters
milliseconds— the longest time to waitpredicate— called with the locked value; true to keep waiting
Returns
- true once
predicatereturned false, false if the time ran out first
Panics
- if the caller does not hold the Mutex
notify_one
notify_one()
Wakes one thread or task waiting on this condition, if any. The caller does not need to hold the Mutex, but usually does, having just changed the value.
notify_all
notify_all()
Wakes every thread and task waiting on this condition. Each locks the Mutex in
turn before its wait() returns.
Semaphore, Barrier and Latch
Coordination between threads: Semaphore bounds how many threads do
something at once, Barrier makes a group of threads meet before any
goes on, and Latch lets threads wait until a count of events has
happened.
Source: runtime/async/coordination.pep
Semaphore
[Shared] struct Semaphore
A count of permits that threads take and give back, like Java’s
Semaphore or C++20 std::counting_semaphore: at most permits threads
hold one at a time.
acquire() waits until a permit is free and takes it; release() gives
one back. The usual form is a with block, which gives the permit back
on every path out: with (semaphore.permit()) { ... }.
A Semaphore is [Shared]: keep it in a module-scope variable or pass it
to threads. Permits are not tied to a thread: any thread may release a
permit that another acquired, and release() may raise the count above
where it started.
In a task (see run), waiting for a permit parks the task, not its worker
thread.
Example
import async;
# At most two downloads at once.
const downloads = async.Semaphore(2);
download(url: String) {
with (downloads.permit()) {
# ... fetch url ...
}
}
Constructor
Semaphore()
construct(permits: ulong)
Creates a Semaphore with permits free permits.
Parameters
permits— how many permits are free at first; 0 makes everyacquire()wait for arelease()
Methods
acquire
acquire()
Waits until a permit is free, and takes it.
Give it back with release(), or use permit() in a with block,
which gives it back for you.
try_acquire
try_acquire(): bool
Takes a permit only if one is free right now, without waiting.
Returns
- true if the caller now holds a permit, false if none was free
release
release()
Gives a permit back, waking one thread waiting in acquire() if any.
permit
permit(): SemaphorePermit
Waits until a permit is free, takes it, and returns a guard that gives
it back when destroyed: at the end of with (semaphore.permit()) { ... }
by any path.
Returns
- the guard holding the permit
Example
with (connections.permit()) {
serve(client);
} # the permit is given back here
SemaphorePermit
struct SemaphorePermit
A permit taken from a Semaphore, given back when the guard is destroyed.
Get one from Semaphore.permit() in a with header.
Example
with (var permit = pool.permit()) {
# holds one of the pool's permits
}
Constructor
SemaphorePermit()
construct(semaphore: Semaphore)
Waits for a permit from semaphore and holds it; the same as
semaphore.permit().
Parameters
semaphore— the Semaphore to take a permit from
Methods
enter
enter(): SemaphorePermit
Returns this guard, for the with header.
Returns
- this permit
exit
exit()
Gives the permit back if it is still held. The destructor calls it; calling it again does nothing.
Barrier
[Shared] struct Barrier
A meeting point for a fixed number of threads, like Rust’s
std::sync::Barrier: each thread that calls wait() sleeps until count
threads have called it, then all of them go on together.
Exactly one thread of each group, the leader, gets true from wait(), so
it can do the work that must happen once per round. The barrier then
resets, and the next count calls make the next round.
A Barrier is [Shared]. In a task (see run), wait() parks the task,
not its worker thread; the other tasks of a round must then run on other
workers or later on the same one.
Example
import async;
const step_done = async.Barrier(4);
work(id: long) {
var step = 0;
while (step < 10) {
# ... this thread's share of the step ...
if (step_done.wait()) {
print($"step {step} done\n"); # once per step
}
step = step + 1;
}
}
Constructor
Barrier()
construct(count: ulong)
Creates a barrier for groups of count threads.
Parameters
count— how many threads each round waits for; 0 counts as 1, so everywait()returns at once as the leader
Methods
wait
wait(): bool
Waits until count threads, this one included, have called wait()
in this round.
Returns
- true on exactly one thread of each round, the last to arrive; false on the others
Latch
[Shared] struct Latch
A one-shot countdown, like C++20 std::latch: threads wait until
count_down() has been called count times. Once open it stays open.
Unlike a Barrier, the threads that count down need not wait, and the
threads that wait need not count down: a typical use is a thread that
starts workers and waits for each to report that it is ready.
A Latch is [Shared]. In a task (see run), wait() parks the task, not
its worker thread.
Example
import async;
const ready = async.Latch(3);
worker() {
# ... set up ...
ready.count_down();
# ... work ...
}
main(): int {
var threads = Array<async.Thread>();
var i = 0;
while (i < 3) {
threads.append(async.Thread.spawn(worker));
i = i + 1;
}
ready.wait(); # every worker has set up
for (var t in threads) {
t.join();
}
return 0;
}
Constructor
Latch()
construct(count: ulong)
Creates a latch that opens after count calls to count_down().
Parameters
count— how many calls open it; 0 makes a latch that is open from the start
Methods
count_down
count_down()
Counts one event, and opens the latch, waking every waiter, if it was the last one.
Panics
- if the latch is already open
wait
wait()
Waits until the latch is open. Returns at once if it already is.
is_ready
is_ready(): bool
Reports whether the latch is open, without waiting.
Returns
- true once
count_down()has been calledcounttimes
RwLock
Reader/writer locking: RwLock<T> owns a thread-safe value that many
threads may read at once or one thread may write, with
RwLockReadGuard<T> and RwLockWriteGuard<T> as the locks a with
block holds.
Source: runtime/async/rwlock.pep
RwLock
[Shared] struct RwLock<T: Shared>
A thread-safe value that many threads may read at once or one thread may
write, like Rust’s RwLock<T> or C++ std::shared_mutex.
with (var v = lock.read()) { ... } waits until no writer holds or is
waiting for the lock, then binds the value; any number of threads may be
reading at once. with (var v = lock.write()) { ... } waits until no
other thread holds the lock at all. Both unlock when the block ends by
any path, and everything a writer did is visible to the threads that lock
after it.
T must be thread-safe, as for Atomic<T>: typically a [Shared] struct
whose Atomic fields a writer changes together, so readers never see
them half changed. A value that is not thread-safe, such as an
Array<String>, is a compile error: readers holding it at once would
race on its reference counts. Use a Mutex<T> for such a value.
Writers come first: while a writer waits, new readers wait too, so readers cannot keep a writer out forever. A thread that already reads must not ask to read again, since a writer may be waiting between the two. Asking to write while holding the lock in either mode panics instead of deadlocking.
In a task (see run), waiting for the lock parks the task, not its worker
thread.
Type parameters
T— the protected value’s type, which must be thread-safe (T: Shared)
Example
import async;
[Shared]
struct Range {
const low: Atomic<long>;
const high: Atomic<long>;
construct() {
low = Atomic<long>(0);
high = Atomic<long>(0);
}
}
const range = async.RwLock<Range>(Range());
widen(by: long) {
with (var r = range.write()) {
r.low.fetch_sub(by);
r.high.fetch_add(by);
}
}
width(): long {
with (var r = range.read()) {
return r.high.load() - r.low.load(); # never half widened
}
return 0;
}
Constructor
RwLock()
construct(value: T)
Creates an unlocked RwLock holding value.
Parameters
value— the value to protect
Methods
read
read(): RwLockReadGuard<T>
Waits until no writer holds or waits for the lock, takes it for
reading, and returns the guard that a with block enters to reach the
value. Other threads may read at the same time.
Returns
- the guard; its
enter()returns the value, and destroying it unlocks
Panics
- if the calling thread holds the lock for writing
Example
with (var config = settings.read()) {
print($"{config.port.load()}\n");
}
try_read
try_read(): RwLockReadGuard<T>
Takes the lock for reading only if that needs no wait: no writer holds it or waits for it.
In with (var v = lock.try_read()) { ... } else { ... }, the body runs
when the lock was taken and the else block runs when it was not.
Returns
- the guard; its
enter()returns the value, or null when the lock was not free for reading
write
write(): RwLockWriteGuard<T>
Waits until no other thread holds the lock, takes it for writing, and
returns the guard that a with block enters to reach the value.
Returns
- the guard; its
enter()returns the value, and destroying it unlocks
Panics
- if the calling thread already holds the lock for writing
Example
with (var config = settings.write()) {
config.port.store(8080);
}
try_write
try_write(): RwLockWriteGuard<T>
Takes the lock for writing only if no thread holds it right now, without waiting.
Returns
- the guard; its
enter()returns the value, or null when the lock was busy
replace
replace(value: T): T
Replaces the whole value under the write lock and returns the one the RwLock held.
Parameters
value— the new value
Returns
- the value the RwLock held
Panics
- if the calling thread already holds the lock for writing
RwLockReadGuard
struct RwLockReadGuard<T: Shared>
The read lock on an RwLock<T> that a with block holds: enter()
returns the value, and destroying the guard unlocks. Get one from
RwLock.read() or RwLock.try_read() in a with header.
Type parameters
T— the protected value’s type
Example
with (var stats = counters.read()) {
total = stats.hits.load() + stats.misses.load();
} # unlocked here
Fields
locked
bool locked
Whether the guard holds the lock: false once exit() has run, and for
a try_read() that could not take it.
Constructor
RwLockReadGuard()
construct(source: RwLock<T>, wait: bool = true)
Takes source for reading and creates the guard that holds it; the
same as source.read() (or source.try_read() when wait is false).
Parameters
source— the RwLock to lockwait— true to wait for the lock; false to take it only if that needs no wait
Panics
- if
waitis true and the calling thread holds the lock for writing
Methods
enter
enter(): T
Returns the RwLock’s value, for the with header to bind.
Returns
- the protected value, or the default of
T(null for a struct) when the guard does not hold the lock
exit
exit()
Unlocks if the guard holds the lock. The destructor calls it; calling it again does nothing.
RwLockWriteGuard
struct RwLockWriteGuard<T: Shared>
The write lock on an RwLock<T> that a with block holds: enter()
returns the value, and destroying the guard unlocks. Get one from
RwLock.write() or RwLock.try_write() in a with header.
Type parameters
T— the protected value’s type
Example
with (var stats = counters.write()) {
stats.hits.store(0);
stats.misses.store(0);
} # unlocked here
Fields
locked
bool locked
Whether the guard holds the lock: false once exit() has run, and for
a try_write() that found the lock busy.
Constructor
RwLockWriteGuard()
construct(source: RwLock<T>, wait: bool = true)
Takes source for writing and creates the guard that holds it; the
same as source.write() (or source.try_write() when wait is
false).
Parameters
source— the RwLock to lockwait— true to wait for the lock; false to take it only if it is free right now
Panics
- if
waitis true and the calling thread already holds the lock for writing
Methods
enter
enter(): T
Returns the RwLock’s value, for the with header to bind.
Returns
- the protected value, or the default of
T(null for a struct) when the guard does not hold the lock
exit
exit()
Unlocks if the guard holds the lock. The destructor calls it; calling it again does nothing.
Once and Lazy
One-time initialization: Once runs a function exactly once however many
threads ask, and Lazy<T> builds a value on first use.
Source: runtime/async/once.pep
Once
[Shared] struct Once
Runs a function exactly once, however many threads ask, like C++
std::call_once or Rust’s std::sync::Once.
The first call runs its body. Callers that arrive while it runs wait for
it to finish, and later callers return right away. Every caller sees
everything the body did once call returns.
A Once is [Shared]: keep it in a module-scope variable or pass it to
threads.
Example
import async;
const setup = async.Once();
const setups = Atomic<long>(0);
initialize() {
setups.fetch_add(1);
}
work() {
setup.call(initialize); # only the first caller runs it
}
Constructor
Once()
construct()
Creates a Once whose body has not run.
Methods
call
call(body: ThreadBody)
Runs body if no call has run a body yet; otherwise waits until that
run has finished. Everything the body did is visible to the caller
when this returns.
Only one body ever runs, even if later calls pass different ones.
Parameters
body— the function to run once
Panics
- if
bodyitself callscallon the same Once (it would wait for itself forever)
Example
config_once.call(load_config);
is_done
is_done(): bool
Reports whether a body has finished running.
Returns
- true once a
callhas completed its body on some thread
Lazy
[Shared] struct Lazy<T: Shared>
A value built on first use by whichever thread asks first, like Rust’s
LazyLock. The usual way to give a module-scope variable a value that is
expensive to build, or that cannot be built before main runs.
The first get() runs the initializer; threads that call get() while it
runs wait for it, and every later call returns the same value.
T is constrained Shared (a thread-safe type such as a number, a
[Shared] struct, or a shared function type), because every thread
gets the same value; Lazy<String> is a compile error.
Type parameters
T— the value’s type, which must be thread-safe (T: Shared)
Example
import async;
[Shared]
struct Settings {
const workers: long;
construct(this.workers = 1) {}
}
load_settings(): Settings {
return Settings(async.logical_cpus() as long);
}
const settings = async.Lazy<Settings>(load_settings);
main(): int {
var workers = settings.get().workers; # built here, on first use
return 0;
}
Constructor
Lazy()
construct(initializer: shared () => T)
Creates a Lazy whose value initializer builds on the first get().
Nothing runs yet.
Parameters
initializer— builds the value; runs at most once, on the thread that first callsget()
Methods
get
get(): T
Returns the value, building it with the initializer on the first call.
For a struct T the caller gets its own reference.
Returns
- the value the initializer built
Panics
- if the initializer itself calls
get()on the same Lazy
is_built
is_built(): bool
Reports whether the value has been built.
Returns
- true once a
get()has finished running the initializer
Channel
Channels: Channel<T> carries values from threads that send to threads
that receive, and ChannelIterator<T> receives in a for loop.
Source: runtime/async/channel.pep
Channel
[Shared] struct Channel<T>
A queue that carries values from threads that send to threads that
receive, like Go channels or Rust’s mpsc, with any number of each.
Values arrive in the order they were sent (from any one sender). A
bounded channel holds at most capacity values, and send waits while it
is full; Channel<T>(0), the default, is unbounded and send never waits.
close() ends the stream: later sends fail, and receivers take what is
left and then get an error. Only close() closes a channel; dropping it
or the threads that use it does not. for (var v in channel) receives
until the channel is closed and empty.
send takes a new value, which nothing on the sending thread references,
and hands it to the receiver without copying it: a constructor call, a
literal, a call that returns a fresh value, move(x) (a variable that
holds the only reference) or copy(x) (a copy, when the sender keeps
using x). Sending a variable that something else still references is a
compile error. A thread-safe value (a number, a [Shared] struct, a
shared function value such as a ThreadBody) passes freely.
A Channel is [Shared]: keep it in a module-scope variable or pass it to
threads.
Type parameters
T— the type of the values carried
Example
import async;
const jobs = async.Channel<long>(64);
const total = Atomic<long>(0);
worker() {
for (var job in jobs) {
total.fetch_add(job);
}
}
main(): int {
var thread = async.Thread.spawn(worker);
jobs.send(20);
jobs.send(22);
jobs.close();
thread.join();
assert(total.load() == 42, "the worker received both jobs");
return 0;
}
Properties
count
var count: ulong { get; }
The number of values waiting to be received. Other threads may change it as soon as it returns.
Constructor
Channel()
construct(capacity: ulong = 0)
Creates an open, empty channel.
Parameters
capacity— how many values may wait beforesendblocks; 0 (the default) for no limit
Example
var bounded = async.Channel<long>(16);
var unbounded = async.Channel<long>();
Methods
send
send([Move] value: T): bool
Queues value, first waiting while a bounded channel is full.
value must be a new value: nothing else may reference it. The caller
hands its reference over and the value itself is queued, with no copy.
Pass move(x) to send a variable’s value, or copy(x) to keep using
x.
In a task (see run), waiting parks the task and its worker runs
other tasks; a cancel or the task’s timeout ends the wait.
Parameters
value— the value to send; a new value, which the compiler checks
Returns
- true if the value was queued, false if the channel is closed or the task’s wait was cancelled or timed out (the value is dropped)
Example
var line = "hello";
lines.send(copy(line));
line.append(" again"); # the receiver still gets "hello"
lines.send(move(line)); # line is null now
try_send
try_send([Move] value: T): bool
Queues value only if there is room right now, without waiting. The
value is dropped when it is not queued.
Parameters
value— the value to send; a new value, as forsend
Returns
- true if the value was queued, false if the channel is full or closed
receive
receive(): Result<T>
Takes the next value, waiting until one arrives.
In a task (see run), waiting parks the task and its worker runs
other tasks; a cancel or the task’s timeout ends the wait.
Returns
- the value, or an error once the channel is closed and empty, or when the task’s wait was cancelled or timed out
Example
var job = jobs.receive();
if (job.is_ok()) {
run(job.value());
}
try_receive
try_receive(): Result<T>
Takes the next value if one is waiting, without waiting.
Returns
- the value, or an error if none is waiting (see
is_closed()to tell an empty channel from a closed one)
receive_for
receive_for(milliseconds: ulong): Result<T>
Like receive(), but gives up after about milliseconds milliseconds.
Parameters
milliseconds— the longest time to wait
Returns
- the value, or an error if the time ran out, once the channel is closed and empty, or when the task’s wait was cancelled
close
close()
Closes the channel and wakes every waiting sender and receiver.
Later sends fail. Values already queued stay, and receivers still get
them; after the last one, receive() returns an error and for loops
end. Closing a closed channel does nothing.
is_closed
is_closed(): bool
Reports whether close() has been called.
Returns
- true once the channel is closed, even while values are still queued
iterate
iterate(): ChannelIterator<T>
Returns an iterator that receives until the channel is closed and
empty. A for loop over the channel calls it.
Several threads may loop over one channel at once; each value goes to exactly one of them.
Returns
- the iterator a
forloop drives
Example
for (var line in lines) {
print(line, "\n");
} # ends once `lines` is closed and empty
ChannelIterator
[Value] struct ChannelIterator<T>
Receives from a Channel<T> in a for loop: next() waits for the next
value and returns false once the channel is closed and empty.
Channel.iterate() returns one; a for loop over a channel uses it
without naming it. It belongs to the thread that runs the loop.
Type parameters
T— the type of the values carried
Example
var values = jobs.iterate();
while (values.next()) {
total = total + values.current();
}
Constructor
ChannelIterator()
construct(source: Channel<T>)
Creates an iterator that receives from source. source.iterate()
does the same.
Parameters
source— the channel to receive from
Methods
next
next(): bool
Waits for the next value.
Returns
- true if a value arrived (read it with
current()), false once the channel is closed and empty
current
current(): T
Returns the value the last successful next() received.
Returns
- the current value
Select
Waiting on several channels at once: Select<E> takes the first value
any of them receives, or sends a value to the first that has room, and
turns what happened into a case of your own enum, which a match then
handles.
Source: runtime/async/select.pep
Select
struct Select<E>
Waits on several channels at once and takes the first value any of them
receives, or sends to the first that has room, like Go’s select.
Each channel to receive from is added with receive(channel, wrap),
where wrap turns one of its values into an E, usually a case of an
enum of your own that carries it. A value to send is added with
send(channel, value, sent), where sent returns the E that says it
was sent. wait() then fires whichever arm is ready first and returns
its E, for a match to handle. The channels may carry different types.
- The value is taken under the channel’s own lock, so no other receiver can take it between “ready” and “take”.
- A send arm’s value goes into its channel only when that arm fires;
until then the select holds it. Once sent, the arm leaves the select,
so the next
wait()does not send it again. A value never sent is dropped with the select. - When several channels have values, each wait starts after the channel that fired last, so a busy channel cannot starve the others.
- A channel that is closed and empty is skipped, and so is a send arm
whose channel is closed. Once every arm is,
wait()returns an error. - In a task (see
run), waiting parks the task and its worker runs other tasks; a cancel or the task’stimeoutends the wait.
A Select is not [Shared]: use it from the thread (or task) that made it.
Type parameters
E— the type every channel’s values are turned into
Example
enum Event {
Job(job: Job),
Tick(count: long),
Stop,
}
var select = async.Select<Event>();
select.receive(jobs, (job: Job): Event => Event.Job(job));
select.receive(ticks, (count: long): Event => Event.Tick(count));
select.receive(quit, (stop: bool): Event => Event.Stop);
var running = true;
while (running) {
match (select.wait()) {
Ok(event) => {
match (event) {
Job(job) => { run(job); }
Tick(count) => { report(count); }
Stop => { running = false; }
}
}
Err(error) => { running = false; } # every channel closed
}
}
Constructor
Select()
construct()
Creates a select with no channels yet.
Methods
receive
receive<T>(channel: Channel<T>, wrap: (T) => E)
Adds channel: its values, turned into an E by wrap, are among
those wait() returns.
Type parameters
T— the type of the channel’s values
Parameters
channel— the channel to receive fromwrap— turns a value received fromchannelinto anE; it runs on the waiting thread
Example
select.receive(lines, (line: String): Event => Event.Line(line));
send
send<T>(channel: Channel<T>, [Move] value: T, sent: () => E)
Adds a send arm: wait() sends value to channel once the channel
has room (a bounded channel that is full waits), and returns what
sent returns.
value must be a new value, as for Channel.send: the select holds
it and moves it into the channel only when this arm fires, so it is
never sent if another arm fires first. Once sent, the arm leaves the
select. A send arm whose channel is closed never fires, and its value
is dropped with the select.
Type parameters
T— the type of the channel’s values
Parameters
channel— the channel to send tovalue— the value to send; a new value, which the compiler checkssent— returns theEthatwait()returns once the value is sent; it runs on the waiting thread
Example
select.send(results, copy(line), (): Event => Event.Sent);
wait
wait(): Result<E>
Fires the first arm that is ready, waiting until one is: takes the next value any of the channels receives, or sends a send arm’s value to a channel with room.
Returns
- the arm’s
E(the value received, turned into anE, or what a send arm’ssentreturned); or an error once every channel is closed (and empty, for a receive arm), when the select has no arms, or when the task’s wait was cancelled or timed out
wait_for
wait_for(milliseconds: ulong): Result<E>
Like wait(), but gives up after about milliseconds milliseconds.
Parameters
milliseconds— the longest time to wait
Returns
- the arm’s
E, as forwait(); or an error if the time ran out, once every channel is closed (and empty, for a receive arm), or when the task’s wait was cancelled
try_wait
try_wait(): Result<E>
Fires an arm if one is ready now, without waiting: takes a value if a channel has one waiting, or sends a send arm’s value if its channel has room.
Returns
- the arm’s
E, as forwait(); or an error if no arm is ready, or once every channel is closed (and empty, for a receive arm)
Processor counts
Processor counts.
Source: runtime/async/cpus.pep
logical_cpus
logical_cpus(): ulong
Returns the number of logical processors the operating system can run threads on: every hardware thread, so a core with hyperthreading (SMT) counts two or more times.
This is the upper bound on how many threads can run at the same moment,
and a sensible default size for a pool of CPU-bound worker threads. Inside
a virtual machine it counts the processors the guest sees. On Linux, a
CPU quota on the process’s cgroup or any cgroup above it (cgroup v2’s
cpu.max, or v1’s cpu.cfs_quota_us over cpu.cfs_period_us, as
docker run --cpus sets) caps the count, rounded up to whole processors,
as Go and the JVM do: a quota of 1.5 processors counts as 2.
Returns
- the number of logical processors, at least 1
See also
physical_cpus
Example
import async;
main(): int {
print(async.logical_cpus(), " logical processors\n");
return 0;
}
physical_cpus
physical_cpus(): ulong
Returns the number of physical processor cores: hardware threads that share a core through hyperthreading (SMT) count once.
On Linux the count comes from the kernel’s CPU topology
(/sys/devices/system/cpu), on macOS from hw.physicalcpu, and on
Windows from GetLogicalProcessorInformationEx. Inside a virtual machine
the guest cannot see the host’s hardware, so this counts the cores the
hypervisor presents. Where the topology is not available, it returns
logical_cpus().
Returns
- the number of physical cores, at least 1 and at most
logical_cpus()
See also
logical_cpus
Tasks
Tasks: functions that run concurrently on a few worker threads, each on its own small stack, switching only where they wait.
run(body) turns the calling thread into the first worker, starts the
others, and runs body as the first task. A task starts others with
Task<T>.spawn_local (on its own worker) or spawn (on any worker), and
gives its worker to the other tasks there whenever it waits: sleep,
yield_now, and Task.join, whenever a socket call would block, and
whenever it waits on a Mutex, Condition or Channel. Nothing
preempts a task, so a task that computes for a long time without waiting
keeps every other task on its worker waiting too.
A task is a green thread (a fiber): an ordinary function with its own stack, so code that waits looks the same as code that does not, and a function called in a task can wait without being marked in any way.
Source: runtime/async/task.pep
TaskError
struct TaskError
Why Task.join did not return the task’s value.
Fields
message
const message: String
A description of what went wrong.
Constructor
TaskError()
construct(message: String)
Creates a task error.
Parameters
message— a description of what went wrong
Methods
is_cancelled
is_cancelled(): bool
Returns whether the task was cancelled, or the task waiting for it was.
Returns
- true when cancellation ended the wait
is_timed_out
is_timed_out(): bool
Returns whether a timeout around the join expired before the task
finished. The task itself carries on.
Returns
- true when a timeout ended the wait
RuntimeOptions
struct RuntimeOptions
Settings for run_with, built by chaining: RuntimeOptions().stack_size(65536).
Constructor
RuntimeOptions()
construct()
Creates the default options: one worker per logical CPU, 256 KiB of
stack per task, the system’s best I/O backend, and up to 64 threads
for run_blocking.
Methods
workers
workers(count: ulong): RuntimeOptions
Sets how many workers (threads that run tasks) the runtime has. The
thread that calls run_with is the first; the rest are new threads.
Tasks from spawn_local run on the worker that started them; tasks
from spawn run on whichever worker is free. The default is
logical_cpus().
Parameters
count— the number of workers
Returns
- these options
Panics
- if
countis 0
blocking_threads
blocking_threads(count: ulong): RuntimeOptions
Sets how many threads run_blocking may start. The runtime starts
them one at a time, when a blocking call finds none idle, and keeps
them until run_with returns. Once there are count, further calls
wait for one of them to be free. The default is 64.
Parameters
count— the most threads for blocking calls
Returns
- these options
Panics
- if
countis 0
backend
backend(kind: Backend): RuntimeOptions
Sets which I/O backend the runtime waits on sockets with. The default,
Backend.Automatic, picks the system’s best (or the one the
PEPPER_IO_BACKEND environment variable names).
Parameters
kind— the backend
Returns
- these options
Note
run_withpanics if the system does not have the backend
stack_size
stack_size(bytes: ulong): RuntimeOptions
Sets how much stack each task reserves. Only the pages a task touches use memory; the rest is address space. A task that uses more than this crashes the program, as a thread that overflows its stack does.
Parameters
bytes— the stack size in bytes (rounded up to whole pages)
Returns
- these options
Panics
- if
bytesis less than 16 KiB
Task
struct Task<T>
A task started with spawn_local or spawn, and the value its body
returns.
Type parameters
T— the type of the body’s result;voidwhen there is none
Example
import async;
main(): int {
async.run(() => {
var a = async.Task<int>.spawn_local(() => { return 20; });
var b = async.Task<int>.spawn_local(() => { return 22; });
print(a.join().value() + b.join().value()); # 42
});
return 0;
}
Constructor
Task()
construct(core: _TaskCore)
Wraps the scheduler’s record of a task. Only spawn_local and spawn
have one; start tasks with those.
Parameters
core— the scheduler’s record of the task
Static methods
spawn_local
static spawn_local(body: () => T): Task<T>
Starts a task on the current worker that runs body. It runs the
next time the running task waits, and may use anything the running
task can, since it stays on this thread.
Parameters
body— the task’s code
Returns
- the task
Panics
- outside
run
Methods
join
join(): Result<T, TaskError>
Waits for the task to finish and returns its value. Other tasks run meanwhile.
Returns
- the body’s result, or a
TaskErrorif the task was cancelled before it finished, the task callingjoinwas cancelled while it waited, or atimeoutaround thejoinexpired first
Panics
- if a task joins itself, or if called outside
runon a task that has not finished
cancel
cancel()
Asks the task to stop: from now on is_cancelled() is true inside it,
sleep returns at once, and its join calls fail. A task is never
stopped by force; it finishes when its body returns.
is_finished
is_finished(): bool
Returns whether the task’s body has returned.
Returns
- true once the task has finished
run
run(body: () => void)
Runs body as the first task of a new runtime, and returns once body
has returned and every task it started has finished. The calling thread
is the first worker; the runtime starts one more thread per logical CPU
after it (see RuntimeOptions.workers). When body returns, the tasks
still running are cancelled (see Task.cancel), on every worker, and
run waits for them to finish.
Parameters
body— the first task
Panics
- if called inside a task, or if every remaining task is waiting on another and none can continue
Example
import async;
main(): int {
async.run(() => {
var t = async.Task<void>.spawn_local(() => {
async.sleep(10);
print("second");
});
print("first");
t.join();
});
return 0;
}
run_with
run_with(options: RuntimeOptions, body: () => void)
Runs body like run, with the given settings.
Parameters
options— the runtime’s settingsbody— the first task
Panics
- if called inside a task, if every remaining task is waiting on another and none can continue, or if the system does not have the backend
optionsasks for
spawn
spawn<T: Shared>(body: shared () => T): Task<T>
Starts a task that runs body on whichever worker is free first, and
returns it. Unlike Task<T>.spawn_local, the task may run on another
thread, in parallel with the calling task, so the body must be a
shared function (it may capture only thread-safe values) and its result
crosses threads, so T is constrained Shared: a thread-safe type such
as a number, a [Shared] struct, or a shared function type
(spawn<String> is a compile error at String).
A task, once started, stays on the worker that started it. Joining and
cancelling work as for spawn_local.
Type parameters
T— the type of the body’s result, which must be thread-safe (T: Shared);voidwhen there is none
Parameters
body— the task’s code
Returns
- the task
Panics
- outside
run
Example
import async;
sum(from: long, to: long): long {
var total: long = 0;
var i = from;
while (i < to) {
total = total + i;
i = i + 1;
}
return total;
}
main(): int {
async.run(() => {
var low = async.spawn<long>(() => { return sum(0, 500000); });
var high = async.spawn<long>(() => { return sum(500000, 1000000); });
print(low.join().value() + high.join().value()); # 499999500000
});
return 0;
}
spawn_on_each_worker
spawn_on_each_worker(body: shared () => void): Array<Task<void>>
Starts one task on every worker, each running body, and returns them in
worker order. Use it to give each worker its own copy of a loop, such as
a server’s accept loop on an io.SharedTcpListener, so that every worker
takes part. Each task runs on its own worker, the calling task’s worker
included; they are otherwise like tasks from spawn, and body must be
a shared function.
Parameters
body— the code each worker runs
Returns
- one task per worker, the first on the first worker
Panics
- outside
run
Example
import async;
import io;
main(): int {
async.run(() => {
var listener = io.SharedTcpListener.bind(io.SocketAddress.parse("0.0.0.0:7000").value()).value();
var loops = async.spawn_on_each_worker(() => {
while (true) {
var accepted = listener.accept();
if (accepted.is_err()) {
return;
}
var (stream, peer) = accepted.value();
async.Task<void>.spawn_local(() => {
stream.write_all("hello");
});
}
});
for (var loop in loops) {
loop.join();
}
});
return 0;
}
run_blocking
run_blocking<T: Shared>(body: shared () => T): T
Runs body on a separate thread and returns what it returns, parking
the calling task meanwhile so the worker keeps running other tasks. Use it
for work that would otherwise hold the worker up: a call that blocks in
the operating system (reading a file, resolving a host name, a C library
that waits) or a long computation.
The runtime keeps a pool of threads for these calls, started as needed up
to RuntimeOptions.blocking_threads. body runs on one of them, so it
must be a shared function and T is constrained Shared, as for
spawn.
On a pool thread, sockets block the thread, and sleep and the other task
functions behave as outside run.
body always runs to the end: cancelling the calling task, or a timeout
around the call expiring, takes effect once run_blocking has returned.
Outside a task, run_blocking just calls body.
Type parameters
T— the type of the body’s result, which must be thread-safe (T: Shared);voidwhen there is none
Parameters
body— the blocking code
Returns
- what
bodyreturns
Example
import async;
main(): int {
async.run(() => {
var ticker = async.Task<void>.spawn_local(() => {
async.sleep(10);
print("the worker kept running");
});
var answer = async.run_blocking<int>(() => {
async.Thread.sleep(50); # blocks a pool thread, not the worker
return 42;
});
print(answer);
ticker.join();
});
return 0;
}
yield_now
yield_now()
Lets every other ready task run before the calling task continues. Does nothing outside a task.
sleep
sleep(milliseconds: ulong)
Waits at least milliseconds. In a task, other tasks run meanwhile, and
a cancelled task returns at once, as does one whose timeout expires
first; outside a task, the thread sleeps.
Parameters
milliseconds— how long to wait
sleep
sleep(duration: os.Duration)
Waits at least duration, the same as sleep(milliseconds). A duration
that is not a whole number of milliseconds rounds up, so the wait is never
shorter than asked.
Parameters
duration— how long to wait
Example
async.sleep(os.Duration.from_seconds(1));
is_cancelled
is_cancelled(): bool
Returns whether the running task has been cancelled (see Task.cancel),
so a long computation can stop early. False outside a task.
Returns
- true when the running task was asked to stop
Deadline
struct Deadline
A time limit on the waits inside a with block, made by timeout.
While the block runs, a socket call that has to wait fails with an
IoError for which is_timed_out() holds once the limit passes,
Task.join fails with a TaskError for which is_timed_out() holds,
and sleep returns early. Code that does not wait is never interrupted.
Timeouts nest: an inner one cannot extend an outer one.
Constructor
Deadline()
construct(milliseconds: ulong)
Creates a limit milliseconds from now. Use timeout instead.
Parameters
milliseconds— how long from now the limit passes
Methods
enter
enter(): Deadline
Starts applying the limit to the running task; a with block calls
it.
Returns
- this deadline
Panics
- outside a task
exit
exit()
Stops applying the limit; the end of the with block calls it.
is_expired
is_expired(): bool
Returns whether the limit has passed.
Returns
- true once the time is up
timeout
timeout(milliseconds: ulong): Deadline
Returns a time limit for a with block: the waits inside it give up
milliseconds from now (see Deadline).
Parameters
milliseconds— how long the block’s waits may take in all
Returns
- the limit, for a
withheader
Example
import async;
import io;
main(): int {
async.run(() => {
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
with (var deadline = async.timeout(50)) {
var accepted = listener.accept(); # nobody connects
assert(accepted.error().is_timed_out(), "timed out");
}
});
return 0;
}
current_backend
current_backend(): Backend
Returns the I/O backend the running runtime waits on sockets with (see
RuntimeOptions.backend).
Returns
- the backend; never
Backend.Automatic
Panics
- outside
run
TaskLocal
Per-task values: TaskLocal<T> gives every task its own copy of a
variable.
Source: runtime/async/task_local.pep
TaskLocal
[Shared] struct TaskLocal<T>
A variable with a separate value in every task, the task counterpart of
ThreadLocal<T>.
Each task that calls set stores its own value, which is released when
the task finishes. A task starts with no value, including one started by
a task that has one: values are not inherited. Tasks that take turns on
one worker thread each see their own value, which a ThreadLocal cannot
give them.
Code that does not run in a task (before async.run, or on a plain
Thread) has one value per thread, as with a ThreadLocal.
For a thread-safe T (a number, a [Shared] struct), get returns the
calling task’s value (the default of T, zero or null, until it sets
one). For any other T, such as a String, the value is reached through
a with block, with (var value = local.enter()) { ... }, and get is a
compile error; set then takes a new value (a constructor call, a string
literal, copy(x)).
A TaskLocal is [Shared], so one object can be used from every task
and thread; it is usually a module-scope variable. Destroying one
releases only the caller’s value, so destroy it only after the tasks that
used it have finished.
Type parameters
T— the type of the per-task value
See also
ThreadLocal
Example
import async;
const request_id = async.TaskLocal<long>();
handle(id: long) {
request_id.set(id);
async.yield_now(); # the other tasks run meanwhile
assert(request_id.get() == id, "still this task's id");
}
main(): int {
async.run(() => {
var tasks = Array<async.Task<void>>();
for (var id in [1, 2, 3]) {
tasks.append(async.spawn<void>(() => {
handle(id);
}));
}
for (var task in tasks) {
task.join();
}
});
return 0;
}
Constructor
TaskLocal()
construct()
Creates a task-local variable. No task has a value yet.
Panics
- if the operating system has no thread-local keys left
Methods
get
get(): T
Returns the calling task’s value.
For a struct T the caller gets its own reference. Only for a
thread-safe T: for any other T, use with (var value = local.enter()).
Returns
- the value this task last stored, or the default of
T(zero or null) if it has not stored one
set
set([Move] value: T)
Stores value as the calling task’s value, replacing (and, for a
struct T, releasing) the one it held. Other tasks’ values are not
affected.
For a T that is not thread-safe, value must be a new value (a
constructor call, a string literal, copy(x)), which the compiler
checks.
Parameters
value— the new value for this task
enter
enter(): TaskLocalGuard<T>
Returns the guard that a with block enters to reach the calling
task’s value: with (var value = local.enter()) { ... }. The body
runs with the value bound; the else block runs when this task has
no value (for a struct T).
Returns
- the guard; its
enter()returns this task’s value
Example
const trail = async.TaskLocal<Array<String>>();
note(step: String) {
if (!trail.is_set()) {
trail.set(Array<String>());
}
with (var steps = trail.enter()) {
steps.append(step.copy());
}
}
is_set
is_set(): bool
Reports whether the calling task has stored a value since it started
or last called clear().
Returns
- true if this task has a value
clear
clear()
Removes the calling task’s value, releasing it for a struct T.
get() returns the default of T afterwards.
TaskLocalGuard
[Isolated] struct TaskLocalGuard<T>
The access to a task’s TaskLocal<T> value that a with block holds:
enter() returns the value. Get one from TaskLocal.enter().
A guard is [Isolated]: it can only be the scope of a with header, and
the compiler checks the block with the confinement rule (see Mutex).
Type parameters
T— the type of the per-task value
Constructor
TaskLocalGuard()
construct(local: TaskLocal<T>)
Creates the guard for the calling task’s value of local; the same as
local.enter().
Parameters
local— the task-local variable
Methods
enter
enter(): T
Returns the calling task’s value, for the with header to bind.
Returns
- this task’s value, or the default of
T(null for a struct) when it has none
exit
exit()
Does nothing: the value stays with the task.
I/O backends
I/O backends: how a task runtime waits for sockets.
Source: runtime/async/driver.pep
Backend
enum Backend
The I/O backend a runtime waits on sockets with (see
RuntimeOptions.backend).
Cases
Automatic
Automatic
The best one the system has: epoll on Linux, kqueue on macOS, IOCP on
Windows. The PEPPER_IO_BACKEND environment variable (epoll,
io_uring, kqueue, iocp or poll) picks another.
Epoll
Epoll
Linux’s epoll, edge-triggered.
IoUring
IoUring
Linux’s io_uring (kernel 5.11 or later), waiting for sockets with one multishot poll request each. Not the default: container seccomp profiles and hardened kernels often refuse it.
Kqueue
Kqueue
The BSD and macOS kqueue, edge-triggered.
Iocp
Iocp
A Windows I/O completion port, fed readiness by AFD poll requests.
Poll
Poll
poll() (WSAPoll on Windows): available everywhere, but each wait
passes every waiting socket to the system, so it suits few
connections. The reference the others are tested against.
The process module
Child processes, and what belongs to the running one, modeled on Rust’s
std::process.
process is a module: bring it in with import process (and write
process.Command) or import names directly with
import [Command, Stream] from process. arguments() and exit() stay
in the prelude, and the module passes them on as well.
- Command:
Commanddescribes a program to start;run()waits for it and collects its output,spawn()starts it and returns aChild, andexec()runs it in place of the running program - Child: a running child process, its piped streams,
and its
ExitStatus - The running process:
id(),executable_path(), andfork(), which starts a copy of it - Finding programs:
find_executable()looks a program up on thePATH,find_companion()looks beside the running program first, andis_executable()checks a file - Signals:
signals()returns a channel that receives Ctrl-C, a request to terminate or a hangup, for a clean shutdown
Example
import process;
main(): int {
var output = process.Command("git").argument("--version").run();
if (output.is_err()) {
print("git is not installed");
return 1;
}
print(output.value().stdout);
return 0;
}
Source: runtime/process/module.pep
Command
Starting a program: Command names it and its arguments, environment,
working directory and standard streams; spawn() starts it and run()
also waits for it and collects what it wrote.
Source: runtime/process/command.pep
Stream
enum Stream
Where a child’s standard input, output or error goes (see
Command.stdin, Command.stdout and Command.stderr).
Cases
Inherit
Inherit
The child shares the parent’s stream: it reads the parent’s input, or writes where the parent writes.
Pipe
Pipe
A pipe connects the stream to the parent, which reaches it through
Child.stdin(), Child.stdout() or Child.stderr().
Null
Null
The stream is the null device: reading it finds the end at once, and what is written to it is thrown away.
Output
struct Output
What a program Command.run() waited for left behind: how it ended and
what it wrote.
Fields
status
var status: ExitStatus
How the program ended.
stdout
var stdout: String
Everything the program wrote to standard output, decoded as UTF-8 (each byte sequence that is not valid UTF-8 becomes U+FFFD); empty when standard output was not piped.
stderr
var stderr: String
Everything the program wrote to standard error, decoded the same way; empty when standard error was not piped.
Constructor
Output()
construct(status: ExitStatus, stdout: String, stderr: String)
Creates an Output from its parts.
Parameters
status— how the program endedstdout— what it wrote to standard outputstderr— what it wrote to standard error
Command
struct Command
Describes a program to start: the program, its arguments, its environment, its working directory, and where its standard streams go.
A builder in the style of Rust’s std::process::Command: each setter
stores its value and returns this same Command, so calls chain, and one
Command can start the program any number of times.
The program is found the way a shell finds it: a name with no path
separator is searched for on the PATH (on Windows, also in the
directory of the running program and the system directories, and .exe
is added when the name has no extension); anything else is a path,
relative to the parent’s current directory.
The child inherits the parent’s environment, changed by environment,
remove_environment and clear_environment. By default it also
inherits the parent’s current directory and its three standard streams;
run() instead pipes standard output and error, and gives standard
input the null device, unless they were set.
Example
import process;
main(): int {
var output = process.Command("clang").arguments(["--version"]).run().value();
if (!output.status.success()) {
print(output.stderr);
return 1;
}
return 0;
}
Fields
program
var program: String
The program to start, as given to the constructor.
Constructor
Command()
construct(program: String)
Creates a Command that starts program with no arguments, the
parent’s environment and current directory, and inherited streams.
Parameters
program— the program: a name to search for on thePATH, or a path
Methods
argument
argument(value: String): Command
Adds one argument after those already added. It reaches the program as one element of its arguments, whatever it holds: no shell splits or expands it. On Windows, where a program receives one command line, it is quoted so that the C runtime splits it back out as it was.
Parameters
value— the argument
Returns
- this Command
arguments
arguments(values: Array<String>): Command
Adds each of values as an argument, in order.
Parameters
values— the arguments
Returns
- this Command
environment
environment(name: String, value: String): Command
Sets the environment variable name to value for the child,
replacing any value it would inherit. On Windows, names are compared
without regard to case.
Parameters
name— the variable’s namevalue— its value
Returns
- this Command
remove_environment
remove_environment(name: String): Command
Leaves the environment variable name out of the child’s
environment, including a value an earlier environment set.
Parameters
name— the variable’s name
Returns
- this Command
clear_environment
clear_environment(): Command
Starts the child with an empty environment, dropping every variable
the parent has and every change made so far; later calls to
environment add to it.
Returns
- this Command
working_directory
working_directory(path: io.Path): Command
Starts the child in path instead of the parent’s current directory.
A relative program path is still resolved against the parent’s
current directory.
Parameters
path— the child’s current directory
Returns
- this Command
stdin
stdin(stream: Stream): Command
Sets where the child’s standard input comes from.
Parameters
stream—Stream.Inherit,Stream.PipeorStream.Null
Returns
- this Command
stdout
stdout(stream: Stream): Command
Sets where the child’s standard output goes.
Parameters
stream—Stream.Inherit,Stream.PipeorStream.Null
Returns
- this Command
stderr
stderr(stream: Stream): Command
Sets where the child’s standard error goes.
Parameters
stream—Stream.Inherit,Stream.PipeorStream.Null
Returns
- this Command
spawn
spawn(): io.IoResult<Child>
Starts the program and returns at once, without waiting for it.
Each stream that was not set is inherited. A piped stream is reached
through the Child; close a piped standard input when done writing, or
a child that reads to the end never finishes. Wait for the child with
Child.wait(): a child that is never waited for stays in the process
table until the parent ends.
Returns
- the running child; an error when the program cannot be started:
is_not_found()when it is not found,is_permission_denied()when it is not executable, or another platform error
Example
var child = process.Command("sort").stdin(process.Stream.Pipe).stdout(process.Stream.Pipe).spawn().value();
child.stdin().write_all("pear\napple\n");
child.stdin().close();
var sorted = child.stdout().read_string().value(); # "apple\npear\n"
child.wait();
run
run(): io.IoResult<Output>
Starts the program, waits for it to end, and returns how it ended with what it wrote.
Standard output and error are piped and collected unless they were set, and standard input is the null device unless it was set. Both outputs are read at once, so a child that fills one pipe while the parent reads the other does not stall. In a task, the reads and the wait park the task rather than blocking its worker.
Returns
- the exit status and output; an error when the program cannot be started (as for
spawn()) or its output cannot be read
Example
var output = process.Command("echo").argument("hello").run().value();
assert(output.status.success(), "echo succeeds");
assert(output.stdout == "hello\n", "and prints its argument");
exec
exec(): io.IoError
Replaces the running program with this one, as a shell’s exec
does: the program receives this process’s signals, and its exit
status becomes this process’s own. A launcher that only hands its
command line on to another program, such as pepper, ends this way.
On POSIX the program takes over this process (execvp): it keeps
the process id, the standard streams and every other descriptor not
marked close-on-exec, and nothing after the call runs. Windows cannot
replace a process, so there the program is started with the standard
streams inherited and waited for, and this process then ends with its
exit status, as process.exit does. While it waits, this process
ignores Ctrl-C and Ctrl-Break, which the program, sharing the
console, receives and handles itself.
Output print has buffered is flushed first. The standard streams
must be left inherited: no process remains to hold a pipe’s other
end. When the call fails, the process carries on unchanged, in its
own working directory.
Returns
- only when the program cannot be started: why, as for
spawn(), or an error withis_invalid_input()when a standard stream was set to something other thanStream.Inherit
Example
import process;
main(args: String[]): int {
# Run git with this program's arguments, as if it were git.
var git = process.Command("git");
ulong index = 1;
while (index < args.count) {
git.argument(args[index]);
index += 1;
}
var error = git.exec();
print($"cannot run git: {error.message}");
return 1;
}
Child
A running child process (Child), the pipes to it, and how it ended
(ExitStatus).
Source: runtime/process/child.pep
ExitStatus
struct ExitStatus
How a child process ended: with an exit code, or (on POSIX) killed by a signal.
Example
var status = process.Command("false").run().value().status;
assert(!status.success(), "false fails");
assert(status.code().value() == 1, "with code 1");
Constructor
ExitStatus()
construct(code: int = 0, signal: int = 0)
Creates an ExitStatus.
Parameters
code— the exit code, when the process exitedsignal— the signal that ended the process, or 0 when it exited
Methods
success
success(): bool
Returns whether the process exited with code 0.
Returns
- true for exit code 0
code
code(): int?
Returns the exit code: the value the process passed to exit or
returned from main. On POSIX only its low 8 bits reach the parent,
so it is from 0 to 255; on Windows it is the whole 32-bit code (an
unhandled exception gives an NTSTATUS such as 0xC0000005, a
negative number here).
Returns
- the exit code, or none when a signal ended the process
signal
signal(): int?
Returns the signal that ended the process (POSIX only; a Windows process always has an exit code).
Returns
- the signal number, such as 9 for
SIGKILL, or none when the process exited
to_string
to_string(): String
Returns how the process ended as text.
Returns
exit code Norsignal N
Child
[NotCopyable("owns a child process handle and cannot be copied")] struct Child
A child process started by Command.spawn(): its id, the pipes to the
streams that were piped, and its exit status once it ends.
Wait for every child with wait() (or until try_wait() returns a
status): on POSIX a child that ended but was never waited for stays in
the process table until the parent ends. Releasing the last reference to
a Child neither waits for it nor kills it; the process runs on.
Example
import process;
main(): int {
var child = process.Command("sleep").argument("10").spawn().value();
child.kill();
var status = child.wait().value();
assert(!status.success(), "killed");
return 0;
}
Constructor
Child()
construct()
Creates a Child that stands for no process: id() is 0, its streams
are closed, and wait(), try_wait() and kill() fail. Use
Command.spawn() to start one.
Methods
id
id(): ulong
Returns the child’s process id, as the operating system knows it. After the child has been waited for, the system may give the id to a new process.
Returns
- the process id
stdin
stdin(): io.File
Returns the pipe to the child’s standard input, for writing. Close it
when done (close()), so that a child reading to the end sees the
end.
Returns
- the pipe, or a closed File when standard input was not
Stream.Pipe
stdout
stdout(): io.File
Returns the pipe from the child’s standard output, for reading. A read returns 0 once the child has ended (or closed its standard output) and everything it wrote has been read.
Returns
- the pipe, or a closed File when standard output was not
Stream.Pipe
stderr
stderr(): io.File
Returns the pipe from the child’s standard error, for reading.
Returns
- the pipe, or a closed File when standard error was not
Stream.Pipe
wait
wait(): io.IoResult<ExitStatus>
Waits for the child to end and returns how it ended. Standard input, when piped, is closed first, so a child reading it to the end can finish. Waiting again returns the same status.
In a task, the wait parks the task (it runs on a run_blocking
thread) rather than blocking its worker. Read piped output before
waiting: a child stalls when a pipe it writes to is full.
Returns
- the exit status, or an error from the system
try_wait
try_wait(): io.IoResult<ExitStatus?>
Returns how the child ended if it has, without waiting.
Returns
- the exit status, or none while the child runs; or an error from the system
kill
kill(): io.IoResult<void>
Ends the child at once: SIGKILL on POSIX, TerminateProcess (with
exit code 1) on Windows. The child still has to be waited for. Killing
a child that has already been waited for does nothing.
Returns
- Ok, or an error from the system
The running process
The running process: its arguments, id, executable and working directory, and how it ends.
Two functions are listed only here, since the module exports them under names the deprecated prelude functions still hold:
arguments(): Array<String>returns the program’s command-line arguments: element 0 is the program as it was started, and the arguments typed after it follow in order.mainreceives the same array when it is declaredmain(args: String[]): int. On Windows they are in the process’s ANSI code page.exit(code: int)ends the program at once with statuscode(only its low 8 bits reach a POSIX parent), after flushing standard output and the other C output streams. Nothing else runs: locals are not released and destructors do not run.
Source: runtime/process/current.pep
fork
fork(): io.IoResult<Child?>
Starts a copy of the running process (POSIX fork): the copy has the
same memory, open files and code, and both go on from here. In the
parent, returns the copy as a Child to wait for; in the copy, returns
none. The copy has only the calling thread, so fork before starting
threads or tasks, and end the copy with exit().
The C standard streams are flushed first, so what print buffered is
written once. Not on Windows or WASI, which start processes only from a
program: there it returns an error.
Returns
- the child in the parent, none in the copy, or an error
Example
import process;
main(): int {
var forked = process.fork();
if (forked.is_err()) {
return 0;
}
if (!forked.value().is_value()) {
process.exit(3);
}
var status = forked.value().value().wait().value();
return status.code().value() == 3 ? 0 : 1;
}
id
id(): ulong
Returns the running process’s id, as the operating system knows it.
Returns
- the process id (
getpidon POSIX,GetCurrentProcessIdon Windows)
executable_path
executable_path(): io.IoResult<io.Path>
Returns the path of the running program’s executable file, as an
absolute path with symbolic links resolved (on Linux the target of
/proc/self/exe; on macOS _NSGetExecutablePath, resolved; on Windows
GetModuleFileNameW). A tool installed alongside others finds them
next to it with executable_path().value().parent().
Returns
- the executable’s path, or an error when the system cannot report it
Example
import process;
main(): int {
var path = process.executable_path().value();
print(path.to_string());
return 0;
}
working_directory
working_directory(): io.IoResult<io.Path>
Returns the process’s current directory, against which relative paths
are resolved. The same as io.Directory.current().
Returns
- the current directory as an absolute path; an error when it cannot be determined, such as
is_not_found()on POSIX when the directory has been removed
change_working_directory
change_working_directory(path: io.Path): io.IoResult<void>
Changes the process’s current directory. This affects the whole
process, every thread included. The same as io.Directory.set_current.
Parameters
path— the new current directory; a relative path is resolved against the old one
Returns
- an Ok result; an error when the directory cannot be entered:
is_not_found()when it is missing,is_not_a_directory()when it is a file, oris_permission_denied()
Finding programs
Finding programs: find_executable looks a program up on the PATH, as
a shell does before running it, find_companion looks beside the running
program first, and is_executable tells whether a file can be run.
Source: runtime/process/find.pep
find_executable
find_executable(name: String): io.Path?
Returns the program name would run as a command: the first file named
name in the directories PATH lists, in order, that this process may
run. A name that holds a path separator is not looked up: it is
returned when it names such a file itself. On Windows, a name with no
extension also matches name.exe.
Parameters
name— the program’s file name, such as"perf"
Returns
- the program’s path, or none when no directory on the
PATHholds it
Example
import process;
main(): int {
var git = process.find_executable("git");
print(git.is_value() ? git.value().to_string() : "git is not installed");
return 0;
}
is_executable
is_executable(path: io.Path): bool
Returns whether path is a file this process may run: on Linux and
macOS, a file with an execute permission bit set; on Windows, any file.
Parameters
path— the file to check
Returns
- true when
pathnames a file that may be run
find_companion
find_companion(name: String): io.Path?
Returns the program name installed beside the running one, else the
one find_executable finds on the PATH: how a tool finds the other
tools of its suite, as pep-test finds pep-build. On Windows, .exe
is added to name when looking beside the running program.
Parameters
name— the program’s file name, such as"pep-build"
Returns
- the program’s path, or none when neither place holds it
Example
import process;
main(): int {
var compiler = process.find_companion("pep-build");
print(compiler.is_value() ? compiler.value().to_string() : "pep-build is not installed");
return 0;
}
Signals
Signals: Ctrl-C, a request to terminate and a hangup, received as values
on a Channel so a program can shut down cleanly.
signals() returns a channel that receives each signal it names. No
program code runs inside an operating-system signal handler: on POSIX the
handler only writes a byte to a pipe, and a runtime thread turns that byte
into a channel message; on Windows the console control handler already
runs on a thread of its own and sends the message itself.
Source: runtime/process/signals.pep
Signal
enum Signal
A signal a program can receive from signals().
Cases
Interrupt
Interrupt
Ctrl-C: POSIX SIGINT, or Windows CTRL_C_EVENT and
CTRL_BREAK_EVENT.
Terminate
Terminate
A request to end: POSIX SIGTERM, or Windows CTRL_CLOSE_EVENT,
CTRL_LOGOFF_EVENT and CTRL_SHUTDOWN_EVENT.
Hangup
Hangup
The terminal closed: POSIX SIGHUP. Windows never sends it.
signals
signals(wanted: Array<Signal>): async.Channel<Signal>
Returns a new channel that receives each of wanted when the process
gets it, instead of the signal’s default action (ending the program).
Receive from it like any channel: receive() waits outside a task and
suspends inside one, and a task can select on it. Every channel that
names a signal receives it. Closing the channel stops it receiving (the
runtime lets go of it at the next signal or signals() call); once no
open channel names a signal, the signal’s default action returns.
Nothing runs inside an operating-system signal handler, so the program
reacts to a signal only by receiving from the channel. On Windows, the
system ends the program a few seconds after Terminate (the console
closing, a log-off or a shutdown) whatever it does, so clean up quickly.
WASI has no signals: the channel never receives.
Parameters
wanted— the signals the channel receives
Returns
- the channel
Example
import process;
import [Signal] from process;
main(): int {
var stop = process.signals([Signal.Interrupt, Signal.Terminate]);
start_serving();
stop.receive();
print("shutting down");
stop_serving();
return 0;
}
The os module
What the operating system provides beyond the running process: environment variables, clocks and time, and the machine and user.
os is a module: bring it in with import os (and write
os.environment("HOME")) or import names directly with
import [Instant, Duration] from os.
- Environment:
environment(name),environment_variables(),set_environment(name, value)andremove_environment(name) - Time:
Duration,Instant(a monotonic clock, for measuring) andSystemTime(the wall clock) - Machine:
system_os(),system_name(),system_version(),architecture(),hostname(), processor counts,total_memory(),page_size() - User:
user_name(),home_directory(),temp_directory(),is_elevated()
Example
import os;
main(): int {
var start = os.Instant.now();
var runtime = os.environment("PEPPER_RUNTIME").value_or("runtime");
print($"runtime at {runtime} on {os.hostname()}");
print($"took {os.Instant.now() - start}");
return 0;
}
Source: runtime/os/module.pep
Environment
The running process’s environment variables: reading them, and setting and removing them for the process itself.
Every read and change the runtime makes goes through one lock, so Pepper
code never races with itself over the environment, on any thread. C code
that calls getenv itself is outside that lock: on POSIX setenv races
with such a call on another thread, as in every language. To give only a
child process a different environment, use process.Command’s
environment, remove_environment and clear_environment instead.
Source: runtime/os/environment.pep
environment
environment(name: String): String?
Returns the value of the environment variable name, or none when it is
not set.
A variable set to the empty string is some empty String, not none. On
Windows names are not case-sensitive (Path finds PATH), and values
are read as UTF-16 and converted to UTF-8, so text outside the ANSI code
page survives.
Parameters
name— the variable’s name
Returns
- its value, or none when it is not set
Example
import os;
main(): int {
var home = os.environment("HOME");
if (home) {
print(home.value());
}
return 0;
}
set_environment
set_environment(name: String, value: String)
Sets the environment variable name to value for the running process,
replacing any value it had.
The process’s own later reads see the change, and so do the children it
starts afterwards. Safe from any thread for Pepper code; C code that calls
getenv on another thread at the same time can race with it on POSIX
(see this page’s introduction). On Windows names are not case-sensitive.
WASI keeps the change inside the program.
Parameters
name— the variable’s namevalue— its new value; may be empty
Panics
- if
nameis empty or contains=or a NUL byte, orvaluecontains a NUL byte
Example
import os;
main(): int {
os.set_environment("GREETING", "hello");
print(os.environment("GREETING").value()); # hello
return 0;
}
remove_environment
remove_environment(name: String)
Removes the environment variable name from the running process.
Removing a variable that is not set does nothing. The same thread-safety
note as set_environment’s applies.
Parameters
name— the variable’s name
Panics
- if
nameis empty or contains=or a NUL byte
Example
import os;
main(): int {
os.remove_environment("GREETING");
print(os.environment("GREETING").is_empty()); # true
return 0;
}
environment_variables
environment_variables(): HashMap<String, String>
Returns every environment variable of the running process, by name.
The map has no order. On Windows it includes the variables whose names
start with =, such as =C:, which cmd.exe uses for each drive’s
current directory; its keys keep the case the system reports, so look a
name up with environment(name) to ignore case. When a POSIX
environment holds one name twice, the first value wins, as getenv
returns it.
Returns
- a new map from each variable’s name to its value
Example
import os;
main(): int {
for (var (name, value) in os.environment_variables()) {
print($"{name}={value}");
}
return 0;
}
Time
Clocks and spans of time: Duration, Instant for measuring, and
SystemTime for the wall clock.
Source: runtime/os/time.pep
Duration
[Value] struct Duration
A span of time, counted in nanoseconds, like Rust’s
std::time::Duration. It is never negative and reaches about 584 years.
Make one with from_seconds, from_milliseconds, from_microseconds or
from_nanoseconds, or by subtracting two Instants. A value struct: it
is copied, compared with ==, and allocates nothing.
Example
import os;
main(): int {
var timeout = os.Duration.from_seconds(2) + os.Duration.from_milliseconds(500);
print(timeout.milliseconds()); # 2500
print(timeout); # 2.5s
return 0;
}
Constructor
Duration()
construct()
Makes a zero-length duration.
Static methods
from_nanoseconds
static from_nanoseconds(nanoseconds: ulong): Duration
Returns a duration of nanoseconds nanoseconds.
Parameters
nanoseconds— the length in nanoseconds
Returns
- the duration
from_microseconds
static from_microseconds(microseconds: ulong): Duration
Returns a duration of microseconds microseconds.
Parameters
microseconds— the length in microseconds
Returns
- the duration
Panics
- if the duration does not fit in a
ulongof nanoseconds
from_milliseconds
static from_milliseconds(milliseconds: ulong): Duration
Returns a duration of milliseconds milliseconds.
Parameters
milliseconds— the length in milliseconds
Returns
- the duration
Panics
- if the duration does not fit in a
ulongof nanoseconds
from_seconds
static from_seconds(seconds: ulong): Duration
Returns a duration of seconds seconds.
Parameters
seconds— the length in seconds
Returns
- the duration
Panics
- if the duration does not fit in a
ulongof nanoseconds (about 584 years)
Methods
nanoseconds
nanoseconds(): ulong
Returns the whole number of nanoseconds in this duration.
Returns
- the length in nanoseconds
microseconds
microseconds(): ulong
Returns the whole number of microseconds in this duration, rounded down.
Returns
- the length in microseconds
milliseconds
milliseconds(): ulong
Returns the whole number of milliseconds in this duration, rounded down.
Returns
- the length in milliseconds
seconds
seconds(): ulong
Returns the whole number of seconds in this duration, rounded down.
Returns
- the length in seconds
seconds_fraction
seconds_fraction(): double
Returns this duration in seconds, with the fraction.
Returns
- the length in seconds, as a
double
is_zero
is_zero(): bool
Returns whether this duration is zero.
Returns
- true for a zero-length duration
operator +
+(other: Duration): Duration
Returns the sum of two durations.
Parameters
other— the duration to add
Returns
- the longer duration
Panics
- if the sum does not fit in a
ulongof nanoseconds
operator -
-(other: Duration): Duration
Returns this duration less other.
Parameters
other— the duration to take away
Returns
- the difference
Panics
- if
otheris longer than this duration: a duration is never negative
operator *
*(times: ulong): Duration
Returns this duration times times over.
Parameters
times— the factor
Returns
- the product
Panics
- if the product does not fit in a
ulongof nanoseconds
operator /
/(parts: ulong): Duration
Returns this duration split into parts equal parts, rounded down to
a whole nanosecond.
Parameters
parts— the divisor
Returns
- the quotient
Panics
- if
partsis zero
operator <
<(other: Duration): bool
Returns whether this duration is shorter than other.
Parameters
other— the duration to compare with
Returns
- true when this one is shorter
operator <=
<=(other: Duration): bool
Returns whether this duration is shorter than or as long as other.
Parameters
other— the duration to compare with
Returns
- true when this one is not longer
operator >
>(other: Duration): bool
Returns whether this duration is longer than other.
Parameters
other— the duration to compare with
Returns
- true when this one is longer
operator >=
>=(other: Duration): bool
Returns whether this duration is longer than or as long as other.
Parameters
other— the duration to compare with
Returns
- true when this one is not shorter
to_string
to_string(): String
Returns this duration as text in the largest unit it reaches, with
the fraction it needs and no trailing zeros: 2.5s, 15ms,
1.25µs, 40ns, 0s.
Returns
- the duration as text
Instant
[Value] struct Instant
A moment on a monotonic clock, for measuring how long something takes,
like Rust’s std::time::Instant.
The clock only moves forward: setting the system’s date and time does
not change it. Its readings mean nothing by themselves, only the
Duration between two of them, and they cannot be compared across
processes or reboots. Use SystemTime for the date and time.
The clock is CLOCK_MONOTONIC on Linux and macOS and
QueryPerformanceCounter on Windows, read to the nanosecond.
Example
import os;
main(): int {
var start = os.Instant.now();
var total = 0;
for (var i in 0..1000000) {
total = total + i;
}
var elapsed = os.Instant.now() - start;
print($"summed in {elapsed.milliseconds()} ms");
return 0;
}
Constructor
Instant()
construct()
Makes the instant the clock started from; use now() to read the
clock.
Static methods
now
static now(): Instant
Reads the monotonic clock.
Returns
- the current moment
Methods
elapsed
elapsed(): Duration
Returns how long ago this instant was: Instant.now() - this.
Returns
- the time since this instant
operator -
-(earlier: Instant): Duration
Returns the time from earlier to this instant.
Parameters
earlier— an instant no later than this one
Returns
- the time between them
Panics
- if
earlieris later than this instant
operator +
+(duration: Duration): Instant
Returns the instant duration after this one.
Parameters
duration— how far to move forward
Returns
- the later instant
Panics
- if the result does not fit the clock
operator -
-(duration: Duration): Instant
Returns the instant duration before this one.
Parameters
duration— how far to move back
Returns
- the earlier instant
Panics
- if the result is before the clock’s start
operator <
<(other: Instant): bool
Returns whether this instant is earlier than other.
Parameters
other— the instant to compare with
Returns
- true when this one is earlier
operator <=
<=(other: Instant): bool
Returns whether this instant is earlier than or the same as other.
Parameters
other— the instant to compare with
Returns
- true when this one is not later
operator >
>(other: Instant): bool
Returns whether this instant is later than other.
Parameters
other— the instant to compare with
Returns
- true when this one is later
operator >=
>=(other: Instant): bool
Returns whether this instant is later than or the same as other.
Parameters
other— the instant to compare with
Returns
- true when this one is not earlier
SystemTime
[Value] struct SystemTime
A date and time on the system’s wall clock, counted in nanoseconds from
the Unix epoch (1970-01-01 00:00:00 UTC), like Rust’s
std::time::SystemTime. It reaches from the year 1677 to 2262.
The wall clock can jump, backwards too, when the system’s time is set,
so measure how long something takes with Instant instead. The time
module turns it into dates, times of day and time zones, and back.
Example
import os;
main(): int {
var now = os.SystemTime.now();
print($"{now.unix_seconds()} seconds since 1970");
return 0;
}
Constructor
SystemTime()
construct()
Makes the Unix epoch, 1970-01-01 00:00:00 UTC.
Static methods
now
static now(): SystemTime
Reads the wall clock.
Returns
- the current date and time
from_unix_nanoseconds
static from_unix_nanoseconds(nanoseconds: long): SystemTime
Returns the time nanoseconds nanoseconds after the Unix epoch.
Parameters
nanoseconds— nanoseconds since the epoch; negative for earlier times
Returns
- the time
from_unix_seconds
static from_unix_seconds(seconds: long): SystemTime
Returns the time seconds seconds after the Unix epoch, as file
systems and most protocols record it.
Parameters
seconds— seconds since the epoch; negative for earlier times
Returns
- the time
Panics
- if the time is outside the years 1677 to 2262
Methods
unix_nanoseconds
unix_nanoseconds(): long
Returns the nanoseconds from the Unix epoch to this time.
Returns
- nanoseconds since the epoch; negative for earlier times
unix_milliseconds
unix_milliseconds(): long
Returns the milliseconds from the Unix epoch to this time, rounded down (towards the past).
Returns
- milliseconds since the epoch; negative for earlier times
unix_seconds
unix_seconds(): long
Returns the seconds from the Unix epoch to this time, rounded down (towards the past).
Returns
- seconds since the epoch; negative for earlier times
duration_since
duration_since(earlier: SystemTime): Duration?
Returns the time from earlier to this time, or none when earlier
is later: the wall clock can be set back between two readings.
Parameters
earlier— the time to measure from
Returns
- the time between them, or none when
earlieris later
elapsed
elapsed(): Duration?
Returns how long ago this time was on the wall clock, or none when it is in the future.
Returns
- the time since this one, or none when it is later than now
operator +
+(duration: Duration): SystemTime
Returns the time duration after this one.
Parameters
duration— how far to move forward
Returns
- the later time
Panics
- if the result is after the year 2262
operator -
-(duration: Duration): SystemTime
Returns the time duration before this one.
Parameters
duration— how far to move back
Returns
- the earlier time
Panics
- if the result is before the year 1677
operator <
<(other: SystemTime): bool
Returns whether this time is earlier than other.
Parameters
other— the time to compare with
Returns
- true when this one is earlier
operator <=
<=(other: SystemTime): bool
Returns whether this time is earlier than or the same as other.
Parameters
other— the time to compare with
Returns
- true when this one is not later
operator >
>(other: SystemTime): bool
Returns whether this time is later than other.
Parameters
other— the time to compare with
Returns
- true when this one is later
operator >=
>=(other: SystemTime): bool
Returns whether this time is later than or the same as other.
Parameters
other— the time to compare with
Returns
- true when this one is not earlier
Machine
The machine the program runs on: its operating system, processor, memory and name.
Source: runtime/os/machine.pep
OperatingSystem
enum OperatingSystem
An operating system Pepper runs on, with the names [Target(OS(...))]
uses.
See also
system_os
Cases
Linux
Linux
Linux.
Mac
Mac
macOS.
Windows
Windows
Windows.
Wasi
Wasi
WASI: a WebAssembly runtime such as wasmtime (--target wasm32-wasip1).
IOS
IOS
iOS and iPadOS, on a device or in the simulator (--target aarch64-apple-ios). [Target(OS(Mac))] covers it too.
Android
Android
Android (--target aarch64-linux-android). [Target(OS(Linux))]
covers it too.
system_os
system_os(): OperatingSystem
Returns the operating system the program was built for.
Returns
- the operating system
Example
import os;
main(): int {
var executable = os.system_os() == os.OperatingSystem.Windows ? "tool.exe" : "tool";
print(executable);
return 0;
}
system_name
system_name(): String
Returns the name of the operating system the program was built for, in
lowercase: "linux", "macos", "windows", "wasi", "ios" or
"android", for printing and for file and directory names.
Returns
- the operating system’s name
See also
system_os
system_version
system_version(): String
Returns the operating system’s version: the kernel release on Linux
(uname -r, such as 6.8.0-45-generic), the product version on macOS
(sw_vers -productVersion, such as 14.5), and the major, minor and
build numbers on Windows (such as 10.0.22631). WASI does not report
one.
Returns
- the version, or an empty String when the system does not report it
architecture
[Target(Arch(x64))] architecture(): String
Returns the processor architecture the program was built for: "x64",
"aarch64" or "wasm32", the names [Target(Arch(...))] uses.
Returns
- the architecture’s name
architecture
[Target(Arch(aarch64))] architecture(): String
Returns the processor architecture the program was built for: "x64",
"aarch64" or "wasm32", the names [Target(Arch(...))] uses.
Returns
- the architecture’s name
architecture
[Target(Arch(wasm32))] architecture(): String
Returns the processor architecture the program was built for: "x64",
"aarch64" or "wasm32", the names [Target(Arch(...))] uses.
Returns
- the architecture’s name
hostname
hostname(): String
Returns the machine’s host name, as hostname prints it (on Windows its
DNS host name). WASI does not report one.
Returns
- the host name, or an empty String when the system does not report it
logical_cpus
logical_cpus(): ulong
Returns the number of logical processors: every hardware thread, so a
core with hyperthreading counts two or more times. On Linux a cgroup
CPU quota (docker run --cpus) caps it, rounded up. The same as
async.logical_cpus().
Returns
- the number of logical processors, at least 1
See also
physical_cpus
physical_cpus
physical_cpus(): ulong
Returns the number of physical processor cores: hardware threads that
share a core count once. The same as async.physical_cpus().
Returns
- the number of physical cores, at least 1 and at most
logical_cpus()
See also
logical_cpus
total_memory
total_memory(): ulong
Returns the machine’s physical memory in bytes. Inside a virtual
machine this is what the guest sees. On Linux, a lower memory limit on
the process’s cgroup or any cgroup above it (cgroup v2’s memory.max
or v1’s memory.limit_in_bytes, as docker run --memory sets) is
returned instead, as the JVM does. WASI does not report it.
Returns
- the total physical memory, or the container’s memory limit when that is lower, in bytes; 0 when the system does not report it
page_size
page_size(): ulong
Returns the size of a page of virtual memory in bytes: the unit the operating system maps memory in, usually 4096 (16384 on Apple silicon; 65536, WebAssembly’s page, on WASI).
Returns
- the page size in bytes
User
The user the program runs as: their name, directories, and whether they have administrator rights.
Source: runtime/os/user.pep
user_name
user_name(): String?
Returns the name of the user the program runs as: on POSIX the login
name of the effective user id (from the user database, else USER),
on Windows GetUserNameW’s answer (else USERNAME), and on WASI
USER.
Returns
- the user name, or none when it cannot be found
is_elevated
is_elevated(): bool
Returns whether the program runs with administrator rights: on POSIX,
as root (effective user id 0); on Windows, from an elevated
(“Run as administrator”) process of a member of Administrators
(IsUserAnAdmin); never on WASI. Tools that need them, such as a
profiler that reads kernel events, check this to say so up front.
Returns
- true when the program runs as root or elevated
home_directory
home_directory(): io.Path?
Returns the user’s home directory: on POSIX HOME, or the user
database’s entry when HOME is unset or empty; on Windows
USERPROFILE; on WASI HOME.
Returns
- the home directory, or none when it cannot be found
Example
import os;
main(): int {
var home = os.home_directory();
if (home) {
print(home.value().join(".config").to_string());
}
return 0;
}
temp_directory
temp_directory(): io.Path
Returns the directory for temporary files: TMPDIR or /tmp on POSIX
(/data/local/tmp on Android), GetTempPathW on Windows. The same as io.Directory.temp().
Returns
- the temporary directory
The time module
Calendar time: dates, times of day, offsets from UTC, time zones, and
formatting and parsing them, on the proleptic Gregorian calendar. Leap
seconds are ignored, as in Unix time. os.SystemTime is the instant
this module turns into dates and back.
time is a module: bring it in with import time (and write
time.Date(2024, 2, 29)) or import names directly with
import [Date, TimeZone] from time.
- Dates and times:
Date,Time,DateTime,Weekday,is_leap_yearanddays_in_month, with no time zone - Offsets:
Offset, a fixed offset from UTC, andOffsetDateTime, an RFC 3339 timestamp, with HTTP dates - Time zones:
TimeZone(UTC, fixed, local, or named from the system’s database),now()andnow_utc()
Every type’s format(pattern) lays it out with C strftime codes:
| Code | Gives | Code | Gives |
|---|---|---|---|
%Y | year, at least four digits (2024) | %H | hour, 00 to 23 |
%C | century (20) | %I | hour, 01 to 12 |
%y | year in the century, 00 to 99 | %p | AM or PM |
%m | month, 01 to 12 | %M | minute, 00 to 59 |
%d | day, 01 to 31 | %S | second, 00 to 59 |
%e | day, space-padded ( 1) | %N | nanoseconds, nine digits; %3N milliseconds |
%j | day of the year, 001 to 366 | %T | %H:%M:%S |
%F | %Y-%m-%d | %R | %H:%M |
%a | weekday, Mon | %z | offset, +0100 |
%A | weekday, Monday | %:z | offset, +01:00 |
%b, %h | month, Jan | %s | Unix seconds |
%B | month, January | %u | ISO weekday, 1 (Monday) to 7 |
%G | ISO 8601 week-numbering year | %w | weekday, 0 (Sunday) to 6 |
%V | ISO 8601 week, 01 to 53 | %n, %t, %% | newline, tab, % |
Names are English, whatever the locale. %z, %:z and %s need an
offset, so only OffsetDateTime.format takes them.
Example
import os;
import time;
main(): int {
var now = time.now();
print(now); # 2024-03-01T09:00:00.25+01:00
print(now.format("%A %e %B %Y, %H:%M")); # Friday 1 March 2024, 09:00
print(now.to_http_date()); # Fri, 01 Mar 2024 08:00:00 GMT
var date = time.Date.parse("2024-02-29").value();
print(date.add_years(1)); # 2025-02-28
return 0;
}
Source: runtime/time/module.pep
Dates and times
Calendar dates and times of day with no time zone: Date, Time,
DateTime and Weekday, on the proleptic Gregorian calendar, plus
is_leap_year and days_in_month.
Source: runtime/time/civil.pep
is_leap_year
is_leap_year(year: int): bool
Returns whether year is a leap year on the Gregorian calendar: divisible
by 4, except centuries not divisible by 400. Year 0 (1 BC) is one.
Parameters
year— the year
Returns
- true when February of
yearhas 29 days
Example
assert(time.is_leap_year(2024) && !time.is_leap_year(1900), "leap years");
days_in_month
days_in_month(year: int, month: int): int
Returns the number of days in month of year: 28 to 31.
Parameters
year— the year, for Februarymonth— the month, 1 (January) to 12 (December)
Returns
- the number of days in that month
Panics
- if
monthis not from 1 to 12
Weekday
enum Weekday
A day of the week. Each case’s value is its ISO 8601 number, Monday 1 to
Sunday 7, so day as int gives that number, and day.name() its English
name.
Cases
Monday
Monday = 1
The first day of the ISO week.
Tuesday
Tuesday
The second day.
Wednesday
Wednesday
The third day.
Thursday
Thursday
The fourth day.
Friday
Friday
The fifth day.
Saturday
Saturday
The sixth day.
Sunday
Sunday
The seventh and last day of the ISO week.
Methods
to_string
to_string(): String
Returns the weekday’s English name, as name() does: Monday.
Returns
- the name
Date
[Value] struct Date
A date on the proleptic Gregorian calendar, which extends the calendar’s rules to years before it was adopted: a year, a month (1 to 12) and a day of the month (1 to 31), with no time of day or time zone.
A value struct: copied, compared with ==, < and the other
comparisons, and never allocated. Years run from -999999 to 999999; year
0 is 1 BC. Its text is ISO 8601’s 2024-02-29.
Example
import time;
main(): int {
var day = time.Date(2024, 2, 28).add_days(1);
print(day); # 2024-02-29
print(day.weekday()); # Thursday
return 0;
}
Properties
year
var year: int { get; }
The year: 2024, or 0 for 1 BC and -1 for 2 BC.
month
var month: int { get; }
The month, 1 (January) to 12 (December).
day
var day: int { get; }
The day of the month, 1 to 31.
Constructor
Date()
construct(year: int = 1970, month: int = 1, day: int = 1)
Makes the date year-month-day; with no arguments, the Unix
epoch’s 1970-01-01.
Parameters
year— the year, from -999999 to 999999month— the month, 1 to 12day— the day of the month, 1 to the month’s last day
Panics
- if the parts do not name a date: use
checkedfor parts that come from outside the program
Static methods
checked
static checked(year: int, month: int, day: int): Date?
Returns the date year-month-day, or none when the parts do not
name one, such as February 30.
Parameters
year— the year, from -999999 to 999999month— the month, 1 to 12day— the day of the month
Returns
- the date, or none
from_days
static from_days(days: long): Date
Returns the date days days after 1970-01-01.
Parameters
days— days since the Unix epoch; negative for earlier dates
Returns
- the date
Panics
- if the date is outside the years -999999 to 999999
parse
static parse(text: String): Result<Date>
Returns the date in the ISO 8601 form 2024-02-29, or none when
text is not a date. Years outside 0 to 9999 take a sign and more
digits (+10000-01-01, -0001-12-31).
Parameters
text— the text to read
Returns
- the date, or an error saying what is wrong with
text
Methods
days
days(): long
Returns the number of days from 1970-01-01 to this date.
Returns
- days since the Unix epoch; negative for earlier dates
weekday
weekday(): Weekday
Returns the day of the week.
Returns
- the weekday
day_of_year
day_of_year(): int
Returns the day of the year, 1 for January 1 to 366 for December 31 of a leap year.
Returns
- the ordinal day
is_leap_year
is_leap_year(): bool
Returns whether this date’s year is a leap year.
Returns
- true when the year has a February 29
days_in_month
days_in_month(): int
Returns the number of days in this date’s month.
Returns
- 28 to 31
add_days
add_days(days: long): Date
Returns the date days days later, or earlier when days is
negative.
Parameters
days— the number of days to move
Returns
- the date
Panics
- if the date is outside the years -999999 to 999999
add_months
add_months(months: long): Date
Returns the date months months later, or earlier when months is
negative. A day past the end of the new month becomes its last day:
a month after January 31 is February 28 or 29.
Parameters
months— the number of months to move
Returns
- the date
Panics
- if the date is outside the years -999999 to 999999
add_years
add_years(years: long): Date
Returns the date years years later, or earlier when years is
negative. February 29 becomes February 28 in a year that is not a
leap year.
Parameters
years— the number of years to move
Returns
- the date
Panics
- if the date is outside the years -999999 to 999999
days_since
days_since(earlier: Date): long
Returns the number of days from earlier to this date: negative when
earlier is later.
Parameters
earlier— the date to count from
Returns
- the number of days between them
at
at(time: Time = Time()): DateTime
Returns the start of this date, midnight, as a DateTime.
Parameters
time— the time of day; midnight by default
Returns
- the date and time
operator <
<(other: Date): bool
Returns whether this date is earlier than other.
Parameters
other— the date to compare with
Returns
- true when this one is earlier
operator <=
<=(other: Date): bool
Returns whether this date is earlier than or the same as other.
Parameters
other— the date to compare with
Returns
- true when this one is not later
operator >
>(other: Date): bool
Returns whether this date is later than other.
Parameters
other— the date to compare with
Returns
- true when this one is later
operator >=
>=(other: Date): bool
Returns whether this date is later than or the same as other.
Parameters
other— the date to compare with
Returns
- true when this one is not earlier
format
format(pattern: String): String
Returns the date as text laid out by pattern, whose % codes are
those of C’s strftime (see the time module). The
time of day is midnight, and an offset code (%z) panics.
Parameters
pattern— the layout, such as"%d %B %Y"
Returns
- the formatted date
Panics
- if
patternhas an unknown code, or an offset code
to_string
to_string(): String
Returns the date in ISO 8601 form: 2024-02-29.
Returns
- the date as text
Time
[Value] struct Time
A time of day with no date or time zone: an hour (0 to 23), a minute, a second and a nanosecond. Leap seconds are not represented, as in Unix time.
A value struct: copied, compared with ==, < and the other
comparisons, and never allocated. Its text is ISO 8601’s 08:30:00,
with a fraction of a second when there is one (08:30:00.25).
Example
import time;
main(): int {
var lunch = time.Time(12, 30);
print(lunch); # 12:30:00
return 0;
}
Properties
hour
var hour: int { get; }
The hour, 0 to 23.
minute
var minute: int { get; }
The minute, 0 to 59.
second
var second: int { get; }
The second, 0 to 59.
nanosecond
var nanosecond: int { get; }
The nanosecond within the second, 0 to 999999999.
Constructor
Time()
construct(hour: int = 0, minute: int = 0, second: int = 0, nanosecond: int = 0)
Makes the time hour:minute:second; with no arguments,
midnight.
Parameters
hour— the hour, 0 to 23minute— the minute, 0 to 59second— the second, 0 to 59nanosecond— the nanosecond within the second, 0 to 999999999
Panics
- if a part is out of range: use
checkedfor parts that come from outside the program
Static methods
checked
static checked(hour: int, minute: int, second: int = 0, nanosecond: int = 0): Time?
Returns the time hour:minute:second, or none when a part is out
of range.
Parameters
hour— the hour, 0 to 23minute— the minute, 0 to 59second— the second, 0 to 59nanosecond— the nanosecond within the second
Returns
- the time, or none
from_nanoseconds
static from_nanoseconds(nanoseconds: long): Time
Returns the time nanoseconds nanoseconds after midnight.
Parameters
nanoseconds— nanoseconds since midnight, less than a day’s
Returns
- the time
Panics
- if
nanosecondsis negative or a day or more
parse
static parse(text: String): Result<Time>
Returns the time in the ISO 8601 form 08:30:00, with an optional
fraction of a second (08:30:00.25), or an error when text is not
a time. The seconds may be left out (08:30), as TOML 1.1 allows.
Parameters
text— the text to read
Returns
- the time, or an error saying what is wrong with
text
Methods
nanoseconds
nanoseconds(): long
Returns the number of nanoseconds since midnight.
Returns
- nanoseconds since midnight
seconds
seconds(): long
Returns the number of whole seconds since midnight.
Returns
- seconds since midnight
operator <
<(other: Time): bool
Returns whether this time is earlier in the day than other.
Parameters
other— the time to compare with
Returns
- true when this one is earlier
operator <=
<=(other: Time): bool
Returns whether this time is earlier than or the same as other.
Parameters
other— the time to compare with
Returns
- true when this one is not later
operator >
>(other: Time): bool
Returns whether this time is later in the day than other.
Parameters
other— the time to compare with
Returns
- true when this one is later
operator >=
>=(other: Time): bool
Returns whether this time is later than or the same as other.
Parameters
other— the time to compare with
Returns
- true when this one is not earlier
format
format(pattern: String): String
Returns the time as text laid out by pattern, whose % codes are
those of C’s strftime (see the time module). The
date is 1970-01-01, and an offset code (%z) panics.
Parameters
pattern— the layout, such as"%H:%M"
Returns
- the formatted time
Panics
- if
patternhas an unknown code, or an offset code
to_string
to_string(): String
Returns the time in ISO 8601 form: 08:30:00, with the fraction of a
second when it is not zero, without trailing zeros (08:30:00.25).
Returns
- the time as text
DateTime
[Value] struct DateTime
A date and a time of day with no time zone, such as a meeting’s
2024-03-01 09:00 written down without saying where. TOML calls it a
local date-time. Attach an offset with OffsetDateTime, or a zone’s
rules with TimeZone.at_local, to name an instant.
A value struct: copied, compared with ==, < and the other
comparisons, and never allocated. Its text is ISO 8601’s
2024-03-01T09:00:00.
Example
import time;
main(): int {
var start = time.DateTime(2024, 3, 1, 9, 0);
var end = start + os.Duration.from_seconds(90 * 60);
print(end); # 2024-03-01T10:30:00
return 0;
}
Properties
date
var date: Date { get; }
The date.
time
var time: Time { get; }
The time of day.
year
var year: int { get; }
The year, as date.year.
month
var month: int { get; }
The month, 1 to 12, as date.month.
day
var day: int { get; }
The day of the month, as date.day.
hour
var hour: int { get; }
The hour, 0 to 23, as time.hour.
minute
var minute: int { get; }
The minute, as time.minute.
second
var second: int { get; }
The second, as time.second.
nanosecond
var nanosecond: int { get; }
The nanosecond within the second, as time.nanosecond.
Constructor
DateTime()
construct( year: int = 1970, month: int = 1, day: int = 1, hour: int = 0, minute: int = 0, second: int = 0, nanosecond: int = 0 )
Makes the date and time from its parts; with no arguments, the Unix epoch’s 1970-01-01T00:00:00.
Parameters
year— the year, from -999999 to 999999month— the month, 1 to 12day— the day of the monthhour— the hour, 0 to 23minute— the minute, 0 to 59second— the second, 0 to 59nanosecond— the nanosecond within the second
Panics
- if the parts do not name a date and time
Static methods
combine
static combine(date: Date, time: Time): DateTime
Returns date at time.
Parameters
date— the datetime— the time of day
Returns
- the date and time
from_unix_nanoseconds
static from_unix_nanoseconds(nanoseconds: long): DateTime
Returns the date and time nanoseconds nanoseconds after
1970-01-01T00:00:00, counting every day as 86400 seconds.
Parameters
nanoseconds— nanoseconds since the epoch; negative for earlier
Returns
- the date and time
from_unix_seconds
static from_unix_seconds(seconds: long, nanosecond: int = 0): DateTime
Returns the date and time seconds seconds after
1970-01-01T00:00:00, counting every day as 86400 seconds, as Unix
time does. Reaches further than a SystemTime.
Parameters
seconds— seconds since the epoch; negative for earliernanosecond— the nanosecond within the second
Returns
- the date and time
Panics
- if
nanosecondis not from 0 to 999999999, or the date is outside the years -999999 to 999999
from_system_time
static from_system_time(time: os.SystemTime): DateTime
Returns the date and time in UTC of a wall-clock reading.
Parameters
time— the instant
Returns
- its date and time in UTC
parse
static parse(text: String): Result<DateTime>
Returns the date and time in the ISO 8601 form
2024-03-01T09:00:00, or an error when text is not one. A space or
a lowercase t may stand for the T, as TOML allows, and the
seconds may be left out.
Parameters
text— the text to read
Returns
- the date and time, or an error saying what is wrong with
text
Methods
unix_seconds
unix_seconds(): long
Returns the number of seconds from 1970-01-01T00:00:00 to this date and time, rounded down, counting every day as 86400 seconds.
Returns
- seconds since the epoch; negative for earlier
to_system_time
to_system_time(): os.SystemTime
Returns this date and time as a SystemTime, reading it as UTC.
Returns
- the instant
Panics
- if the date is outside what a
SystemTimereaches, the years 1677 to 2262
weekday
weekday(): Weekday
Returns the day of the week.
Returns
- the weekday
operator +
+(duration: os.Duration): DateTime
Returns the date and time duration later.
Parameters
duration— how far to move forward
Returns
- the later date and time
Panics
- if the result is past the year 999999
operator -
-(duration: os.Duration): DateTime
Returns the date and time duration earlier.
Parameters
duration— how far to move back
Returns
- the earlier date and time
Panics
- if the result is before the year -999999
operator <
<(other: DateTime): bool
Returns whether this date and time is earlier than other.
Parameters
other— the date and time to compare with
Returns
- true when this one is earlier
operator <=
<=(other: DateTime): bool
Returns whether this date and time is earlier than or the same as
other.
Parameters
other— the date and time to compare with
Returns
- true when this one is not later
operator >
>(other: DateTime): bool
Returns whether this date and time is later than other.
Parameters
other— the date and time to compare with
Returns
- true when this one is later
operator >=
>=(other: DateTime): bool
Returns whether this date and time is later than or the same as
other.
Parameters
other— the date and time to compare with
Returns
- true when this one is not earlier
format
format(pattern: String): String
Returns the date and time as text laid out by pattern, whose %
codes are those of C’s strftime (see the
time module). An offset code (%z) panics.
Parameters
pattern— the layout, such as"%Y-%m-%d %H:%M"
Returns
- the formatted date and time
Panics
- if
patternhas an unknown code, or an offset code
to_string
to_string(): String
Returns the date and time in ISO 8601 form: 2024-03-01T09:00:00,
with the fraction of a second when it is not zero.
Returns
- the date and time as text
Offsets
Fixed offsets from UTC, Offset, and the date and time at one,
OffsetDateTime: an instant written the way RFC 3339 and TOML write it.
Source: runtime/time/offset.pep
Offset
[Value] struct Offset
A fixed offset from UTC, east positive: +02:00 for Central European
Summer Time, -05:00 for US Eastern Standard Time, +00:00 for UTC.
A value struct: copied, compared with ==, and never allocated. It is
less than a day either way, to the second. Its text is +02:00, with the
seconds when there are any (+00:19:32).
Example
import time;
main(): int {
var india = time.Offset.from_hours(5, 30);
print(india); # +05:30
print(india.seconds); # 19800
return 0;
}
Properties
seconds
var seconds: int { get; }
The offset in seconds, east of UTC positive.
Constructor
Offset()
construct(seconds: int = 0)
Makes the offset seconds seconds east of UTC (west when negative);
with no argument, UTC itself.
Parameters
seconds— the offset in seconds, east positive
Panics
- if the offset is a day or more either way
Static methods
utc
static utc(): Offset
Returns UTC, the zero offset.
Returns
- the zero offset
from_hours
static from_hours(hours: int, minutes: int = 0): Offset
Returns the offset hours hours and minutes minutes from UTC. Both
take the offset’s sign: from_hours(-3, -30) is -03:30, and
from_hours(0, -30) is -00:30.
Parameters
hours— whole hours, east positiveminutes— minutes on top of the hours, with the same sign
Returns
- the offset
Panics
- if the offset is a day or more either way
parse
static parse(text: String): Result<Offset>
Returns the offset written as +02:00, -0530, +02 or Z (UTC),
or an error when text is not one.
Parameters
text— the text to read
Returns
- the offset, or an error saying what is wrong with
text
Methods
is_utc
is_utc(): bool
Returns whether this is UTC, the zero offset.
Returns
- true for
+00:00
to_string
to_string(): String
Returns the offset as text: +02:00, -05:30, +00:00, with the
seconds when there are any (+00:19:32).
Returns
- the offset as text
OffsetDateTime
[Value] struct OffsetDateTime
A date and time at a fixed offset from UTC, such as
2024-03-01T09:00:00+01:00: an instant, together with the local date and
time it was written in. This is RFC 3339’s timestamp and TOML’s offset
date-time.
A value struct: copied and never allocated. == compares the date, time
and offset, so the same instant at two offsets is not ==; compare
instants with to_system_time(), or with < and the other comparisons,
which compare instants. Its text is RFC 3339’s, with Z for UTC.
Example
import time;
main(): int {
var meeting = time.OffsetDateTime.parse("2024-03-01T09:00:00+01:00").value();
print(meeting.to_utc()); # 2024-03-01T08:00:00Z
print(meeting.to_http_date()); # Fri, 01 Mar 2024 08:00:00 GMT
return 0;
}
Properties
date_time
var date_time: DateTime { get; }
The local date and time at offset.
offset
var offset: Offset { get; }
The offset from UTC that date_time is at.
date
var date: Date { get; }
The local date, as date_time.date.
time
var time: Time { get; }
The local time of day, as date_time.time.
Constructor
OffsetDateTime()
construct(date_time: DateTime = DateTime(), offset: Offset = Offset())
Makes the date and time date_time at offset; with no arguments,
the Unix epoch, 1970-01-01T00:00:00Z.
Parameters
date_time— the local date and timeoffset— its offset from UTC
Static methods
from_system_time
static from_system_time(time: os.SystemTime, offset: Offset = Offset()): OffsetDateTime
Returns the date and time of a wall-clock reading at offset.
Parameters
time— the instantoffset— the offset to show it at; UTC by default
Returns
- the instant’s date and time at
offset
parse
static parse(text: String): Result<OffsetDateTime>
Returns the RFC 3339 timestamp in text, such as
2024-03-01T09:00:00+01:00 or 2024-03-01T08:00:00.5Z, or an error
when it is not one. A space or a lowercase t may stand for the T
and a lowercase z for the Z, and the seconds may be left out, as
TOML allows.
Parameters
text— the text to read
Returns
- the date and time, or an error saying what is wrong with
text
parse_http_date
static parse_http_date(text: String): Result<OffsetDateTime>
Returns the HTTP date in text, as RFC 9110 defines it, or an error
when it is not one. It reads the preferred IMF-fixdate
(Sun, 06 Nov 1994 08:49:37 GMT) and the two obsolete forms that
servers must still accept: RFC 850’s (Sunday, 06-Nov-94 08:49:37 GMT, where a two-digit year from 70 is in the 1900s and below 70 in
the 2000s) and C’s asctime (Sun Nov 6 08:49:37 1994). The
weekday must match the date.
Parameters
text— the text to read
Returns
- the date and time in UTC, or an error saying what is wrong with
text
Methods
unix_seconds
unix_seconds(): long
Returns the seconds from the Unix epoch to this instant, rounded down.
Returns
- seconds since 1970-01-01T00:00:00Z; negative for earlier
to_system_time
to_system_time(): os.SystemTime
Returns this instant as a SystemTime.
Returns
- the instant
Panics
- if the instant is outside what a
SystemTimereaches, the years 1677 to 2262
to_offset
to_offset(offset: Offset): OffsetDateTime
Returns the same instant at another offset.
Parameters
offset— the offset to show the instant at
Returns
- the date and time at
offset
to_utc
to_utc(): OffsetDateTime
Returns the same instant in UTC.
Returns
- the date and time at offset zero
weekday
weekday(): Weekday
Returns the day of the week of the local date.
Returns
- the weekday
operator +
+(duration: os.Duration): OffsetDateTime
Returns the instant duration later, at the same offset.
Parameters
duration— how far to move forward
Returns
- the later date and time
operator -
-(duration: os.Duration): OffsetDateTime
Returns the instant duration earlier, at the same offset.
Parameters
duration— how far to move back
Returns
- the earlier date and time
operator <
<(other: OffsetDateTime): bool
Returns whether this instant is earlier than other, whatever their
offsets.
Parameters
other— the instant to compare with
Returns
- true when this one is earlier
operator <=
<=(other: OffsetDateTime): bool
Returns whether this instant is earlier than or the same as other.
Parameters
other— the instant to compare with
Returns
- true when this one is not later
operator >
>(other: OffsetDateTime): bool
Returns whether this instant is later than other, whatever their
offsets.
Parameters
other— the instant to compare with
Returns
- true when this one is later
operator >=
>=(other: OffsetDateTime): bool
Returns whether this instant is later than or the same as other.
Parameters
other— the instant to compare with
Returns
- true when this one is not earlier
format
format(pattern: String): String
Returns the date and time as text laid out by pattern, whose %
codes are those of C’s strftime (see the
time module). %z is the offset as +0200, %:z
as +02:00, and %s the Unix seconds.
Parameters
pattern— the layout, such as"%Y-%m-%d %H:%M %z"
Returns
- the formatted date and time
Panics
- if
patternhas an unknown code
to_http_date
to_http_date(): String
Returns the instant as an HTTP date in UTC, the IMF-fixdate of RFC
9110 that Date and Last-Modified headers carry:
Fri, 01 Mar 2024 08:00:00 GMT. The fraction of a second is dropped.
Returns
- the date as an HTTP date
to_string
to_string(): String
Returns the date and time as an RFC 3339 timestamp:
2024-03-01T09:00:00+01:00, Z for UTC, with the fraction of a
second when it is not zero.
Returns
- the date and time as text
Time zones
Time zones: TimeZone, the rules that give an instant’s offset from UTC,
for UTC, a fixed offset, the system’s local zone, or a named zone such as
Europe/Paris from the system’s time zone database; and now().
Source: runtime/time/zone.pep
TimeZone
struct TimeZone
The rules that give the offset from UTC at each instant in one place:
UTC, a fixed offset, the system’s local time zone, or a named zone from
the IANA time zone database, such as Europe/Paris or
America/New_York, with its daylight saving time and its history.
Named zones come from the system’s database, nothing is bundled: the
files under /usr/share/zoneinfo on Linux and macOS, and ICU’s
(icu.dll, Windows 10 version 1903 and later) on Windows. On every
system, a folder of database files that the TZDIR environment variable
names takes their place. Leap seconds are ignored, as in Unix time.
A zone is a reference, and it does not change once made, so copies share it.
Example
import os;
import time;
main(): int {
var paris = time.TimeZone.named("Europe/Paris").value();
var instant = time.OffsetDateTime.parse("2024-07-14T10:00:00Z").value();
print(paris.at(instant.to_system_time())); # 2024-07-14T12:00:00+02:00
var local = paris.at_local(time.DateTime(2024, 12, 25, 18, 0));
print(local.offset); # +01:00
return 0;
}
Properties
name
var name: String { get; }
The zone’s name: UTC, Local, the offset of a fixed zone
(+05:30), or the database name of a named one (Europe/Paris).
Constructor
TimeZone()
construct()
Makes UTC; the same as TimeZone.utc().
Static methods
utc
static utc(): TimeZone
Returns UTC.
Returns
- the zone whose offset is always zero
fixed
static fixed(offset: Offset): TimeZone
Returns a zone whose offset is always offset.
Parameters
offset— the offset from UTC
Returns
- the zone, named after the offset (
+05:30)
local
static local(): TimeZone
Returns the system’s local time zone, as the C library’s localtime
sees it: the TZ environment variable or /etc/localtime on Linux
and macOS, and the zone in Windows’ settings. WebAssembly (WASI) has
no local zone, so it is UTC there.
Returns
- the local zone, named
Local
named
static named(name: String): Result<TimeZone>
Returns the zone name from the system’s time zone database, such
as Europe/Paris, America/New_York or Asia/Kolkata, or an error
when the system does not know it. UTC is always known.
Parameters
name— the IANA name of the zone, which is case-sensitive
Returns
- the zone, or an error saying it was not found
Methods
offset_at
offset_at(time: os.SystemTime): Offset
Returns the offset from UTC in this zone at time.
Parameters
time— the instant
Returns
- the offset in effect then
at
at(time: os.SystemTime): OffsetDateTime
Returns the date and time in this zone at time.
Parameters
time— the instant
Returns
- its local date and time, with the offset in effect
Example
var here = time.TimeZone.local().at(os.SystemTime.now());
at_local
at_local(local: DateTime): OffsetDateTime
Returns the instant that a local date and time names in this zone, with its offset.
Where the clocks go back, a local time happens twice; this returns
the earlier, at the offset from before the change. Where they go
forward, a local time never happens; this moves it forward by the
length of the gap, so 02:30 on a night that skips from 02:00 to
03:00 is 03:30. This is what Java’s ZonedDateTime.of does.
Parameters
local— the date and time on the zone’s clocks
Returns
- the date and time with the zone’s offset at that instant
now
now(): OffsetDateTime
Returns the current date and time in the system’s local time zone.
Returns
- now, at the local offset
Example
print(time.now()); # such as 2024-03-01T09:00:00.123456789+01:00
now_utc
now_utc(): OffsetDateTime
Returns the current date and time in UTC.
Returns
- now, at offset zero
The math module
Numbers beyond the operators: the math library as it grows.
Bring it in with import math. The floating-point functions (sine,
logarithm, cube_root, is_nan, …) are methods of float and
double, which every program has without an import; this module holds
the constants and the integer functions.
- Constants:
PI,TAU,E,INFINITY,NAN, and the epsilon and limits offloatanddouble - Integers:
gcdandlcm - Parsing:
parse_int, deprecated: every number type has a staticparse(long.parse(text, base)) without an import
Example
import math;
var radius = 2.0;
var area = math.PI * radius ** 2;
assert((math.PI / 2).sine() == 1.0, "a quarter turn");
Source: runtime/math/module.pep
Constants
Constants for floating-point math: π, e, the infinities and NaN, and
the limits of float and double.
Each is a double unless its name starts with FLOAT_; write
math.PI as float for a float.
Source: runtime/math/constants.pep
PI
const PI: double = 3.141592653589793
π, the ratio of a circle’s circumference to its diameter.
TAU
const TAU: double = 6.283185307179586
τ = 2π, the angle of a full turn in radians.
E
const E: double = 2.718281828459045
e, the base of the natural logarithm.
INFINITY
const INFINITY: double = 1.0 / 0.0
Positive infinity; -math.INFINITY is negative infinity.
NAN
const NAN: double = 0.0 / 0.0
A NaN (not a number). It is not equal to anything, itself included, so
test for it with is_nan(), never ==.
DOUBLE_EPSILON
const DOUBLE_EPSILON: double = 2.220446049250313e-16
The gap between 1 and the next larger double, 2^-52.
DOUBLE_MAX
const DOUBLE_MAX: double = 1.7976931348623157e308
The largest finite double.
DOUBLE_MIN_POSITIVE
const DOUBLE_MIN_POSITIVE: double = 2.2250738585072014e-308
The smallest positive normal double, 2^-1022.
FLOAT_EPSILON
const FLOAT_EPSILON: float = 1.1920928955078125e-7
The gap between 1 and the next larger float, 2^-23.
FLOAT_MAX
const FLOAT_MAX: float = 3.4028234663852886e38
The largest finite float.
FLOAT_MIN_POSITIVE
const FLOAT_MIN_POSITIVE: float = 1.1754943508222875e-38
The smallest positive normal float, 2^-126.
Integers
Greatest common divisors and least common multiples of integers.
For integer powers, use **: 3 ** 4 is 81, and it panics on overflow.
Source: runtime/math/integers.pep
gcd
gcd(a: long, b: long): long
Returns the greatest common divisor of a and b: the largest integer
that divides both. It is never negative, and gcd(0, 0) is 0.
Parameters
a— the first integerb— the second integer
Returns
- the greatest common divisor
Panics
- if
aorbis the smallestlong, whose magnitude does not fit
Example
assert(math.gcd(12, -18) == 6, "divides both");
lcm
lcm(a: long, b: long): long
Returns the least common multiple of a and b: the smallest
non-negative integer both divide. It is 0 when either is 0.
Parameters
a— the first integerb— the second integer
Returns
- the least common multiple
Panics
- if the result does not fit a
long
Example
assert(math.lcm(4, 6) == 12, "both divide 12");
Parsing integers
Reading integers from text.
Source: runtime/math/parse.pep
parse_int
[Deprecated("use 'long.parse' instead")] parse_int(text: String, base: int = 10): long?
Deprecated: use long.parse instead, which every program has without an
import (each number type has a static parse).
Parses text as an integer written in base: an optional + or -,
then one or more digits, and nothing else.
Parameters
text— the text to parsebase— the base the digits are written in, from 2 to 36; 10 by default
Returns
- the value, or none when
textis not an integer inbaseor the value does not fit along
Panics
- if
baseis less than 2 or greater than 36
The hint module
Hints to the optimizer about memory a loop will soon read or write.
Bring it in with import hint. A hint never changes what a program
computes, only how fast: a prefetch starts loading a cache line early.
Where the CPU has no prefetch instruction it does nothing.
To say which way a branch usually goes, write [Likely] or
[Unlikely] above the if or match arm; likely and unlikely
here are deprecated.
Example
import hint;
main(): int {
var text = "a long text that a loop will read";
hint.prefetch_read(text.c_str());
assert(text.length > 0, "a prefetch changes nothing");
return 0;
}
Source: runtime/hint/module.pep
likely
[Deprecated("write '[Likely]' above the 'if' instead")] likely(condition: bool): bool
Deprecated: write [Likely] above the if instead.
Returns condition, telling the optimizer it is usually true
(llvm.expect).
Parameters
condition— the condition to test
Returns
condition
unlikely
[Deprecated("write '[Unlikely]' above the 'if' instead")] unlikely(condition: bool): bool
Deprecated: write [Unlikely] above the if instead.
Returns condition, telling the optimizer it is usually false
(llvm.expect).
Parameters
condition— the condition to test
Returns
condition
prefetch_read
prefetch_read(address: __ptr<void>)
Starts loading the cache line holding address for reading, so a read
of it soon after does not wait on memory (llvm.prefetch). It never
faults, even for an address that is not mapped, and does nothing where
the CPU has no prefetch instruction.
Parameters
address— memory that will be read soon
Example
hint.prefetch_read(text.c_str());
prefetch_write
prefetch_write(address: __ptr<void>)
Starts loading the cache line holding address for writing, so a
store to it soon after does not wait on memory (llvm.prefetch). It
never faults, even for an address that is not mapped, and does nothing
where the CPU has no prefetch instruction.
Parameters
address— memory that will be written soon
The simd module
SIMD vectors: Vector<T, N> holds N numbers of type T and computes
on all of them at once, in the CPU’s vector registers.
Bring it in with import simd. T is an integer or floating-point type
(byte, char, short, ushort, int, uint, long, ulong,
float, double) and N, the lane count, is a power of two from 1 to
64, written as a number: simd.Vector<byte, 16>. A vector is a value
([Value]): it is stored inline, copied like a number, and safe to share
between threads.
The operations compile to the target’s vector instructions: SSE2 on
x86-64 (AVX2 with -march=x86-64-v3), NEON on arm64, and simd128 on
WebAssembly with -msimd128. A target without vector registers gets the
same results from scalar code. Sixteen bytes (Vector<byte, 16>,
Vector<int, 4>, Vector<double, 2>) is the width every target handles
well; wider vectors use several registers where the target has no wider
ones.
Lane-wise comparisons (less, equal, …) give a Mask<N>, which
selects lanes (select), answers any and all, and packs into a bit
mask with to_bits, whose trailing_zeros finds the first match.
Example
import simd;
main(): int {
var a = simd.Vector<int, 4>(10);
var b = a + simd.Vector<int, 4>(5);
assert(b[0] == 15 && b.sum() == 60, "lane-wise add");
var over = b.greater(12);
assert(over.all() && over.to_bits() == 0xF, "a mask per lane");
return 0;
}
Source: runtime/simd/module.pep
Lane
trait Lane
The number types a Vector holds: every integer and floating-point
type. The runtime declares the conformances; another type cannot be a
lane.
IntegerLane
trait IntegerLane
The integer lane types, whose vectors have checked division, the bitwise operators, shifts and rotates.
FloatLane
trait FloatLane
The floating-point lane types, whose vectors have floating-point
division, square_root, floor and ceiling.
Vector
[Value] struct Vector<T: Lane, N>
N lanes of the number type T, computed on together.
Integer +, - and * wrap around on overflow in each lane, as vector
instructions do, where scalar arithmetic panics. Integer / and %
panic on a zero divisor lane; shifts panic on a count outside the lane’s
width. Every other operation is defined for every input.
Type parameters
T— the lane type: an integer or floating-point typeN— the lane count: a power of two from 1 to 64
Properties
count
var count: int { get; }
The number of lanes, N.
Constructors
Vector()
construct()
Makes a vector with every lane 0.
Vector()
construct(value: T)
Makes a vector with every lane value.
Parameters
value— the value of every lane
Static methods
load
static load(pointer: __ptr<T>): Vector<T, N>
Reads N values from pointer, which need not be aligned.
Parameters
pointer— the first ofNreadable values
Returns
- a vector of the values, the first in lane 0
load_aligned
static load_aligned(pointer: __ptr<T>): Vector<T, N>
Reads N values from pointer, which must be aligned to the
vector’s size (N times the size of T).
Parameters
pointer— the first ofNreadable values
Returns
- a vector of the values, the first in lane 0
Panics
- if
pointeris not aligned to the vector’s size
load
static load(array: Array<T>, index: ulong): Vector<T, N>
Reads the N elements of array from index on.
Parameters
array— the array to readindex— the element that goes in lane 0
Returns
- a vector of
array[index]througharray[index + N - 1]
Panics
- if
arrayhas fewer thanindex + Nelements
Methods
store
store(pointer: __ptr<T>)
Writes the lanes to the N values at pointer, which need not be
aligned.
Parameters
pointer— the first ofNwritable values
store_aligned
store_aligned(pointer: __ptr<T>)
Writes the lanes to the N values at pointer, which must be
aligned to the vector’s size.
Parameters
pointer— the first ofNwritable values
Panics
- if
pointeris not aligned to the vector’s size
store
store(array: Array<T>, index: ulong)
Writes the lanes to the N elements of array from index on.
Parameters
array— the array to writeindex— the element lane 0 goes to
Panics
- if
arrayhas fewer thanindex + Nelements
operator []
[](index: ulong): T
Returns lane index.
Parameters
index— the lane to read
Returns
- the value in lane
index
Panics
- if
indexis not less thanN
operator []
[](index: ulong, value: T): T
Sets lane index to value.
Parameters
index— the lane to writevalue— the new value of the lane
Returns
value
Panics
- if
indexis not less thanN
operator +
+(other: Vector<T, N>): Vector<T, N>
Adds each lane of other to the same lane of this vector. Integer
lanes wrap around on overflow.
Parameters
other— the vector to add
Returns
- the lane-wise sums
operator +
+(other: T): Vector<T, N>
Adds other to every lane. Integer lanes wrap around on overflow.
Parameters
other— the value to add
Returns
- the lane-wise sums
operator -
-(other: Vector<T, N>): Vector<T, N>
Subtracts each lane of other from the same lane of this vector.
Integer lanes wrap around on overflow.
Parameters
other— the vector to subtract
Returns
- the lane-wise differences
operator -
-(other: T): Vector<T, N>
Subtracts other from every lane. Integer lanes wrap around on
overflow.
Parameters
other— the value to subtract
Returns
- the lane-wise differences
operator *
*(other: Vector<T, N>): Vector<T, N>
Multiplies each lane by the same lane of other. Integer lanes wrap
around on overflow.
Parameters
other— the vector to multiply by
Returns
- the lane-wise products
operator *
*(other: T): Vector<T, N>
Multiplies every lane by other. Integer lanes wrap around on
overflow.
Parameters
other— the value to multiply by
Returns
- the lane-wise products
operator -
-(): Vector<T, N>
Negates every lane. Integer lanes wrap around: the lowest value stays itself.
Returns
- the negated lanes
operator ==
==(other: Vector<T, N>): bool
Whether every lane equals the same lane of other. A NaN lane equals
nothing. For a mask per lane, use equal.
Parameters
other— the vector to compare with
Returns
- true when all lanes are equal
operator !=
!=(other: Vector<T, N>): bool
Whether some lane differs from the same lane of other.
Parameters
other— the vector to compare with
Returns
- true when any lane differs
equal
equal(other: Vector<T, N>): Mask<N>
Compares each lane with the same lane of other for equality.
Parameters
other— the vector to compare with
Returns
- a mask, true in each lane that is equal
equal
equal(other: T): Mask<N>
Compares every lane with other for equality.
Parameters
other— the value to compare with
Returns
- a mask, true in each lane equal to
other
not_equal
not_equal(other: Vector<T, N>): Mask<N>
Compares each lane with the same lane of other for inequality. A
NaN lane differs from everything.
Parameters
other— the vector to compare with
Returns
- a mask, true in each lane that differs
not_equal
not_equal(other: T): Mask<N>
Compares every lane with other for inequality.
Parameters
other— the value to compare with
Returns
- a mask, true in each lane that differs from
other
less
less(other: Vector<T, N>): Mask<N>
Tests whether each lane is less than the same lane of other.
Parameters
other— the vector to compare with
Returns
- a mask, true in each lane that is less
less
less(other: T): Mask<N>
Tests whether every lane is less than other.
Parameters
other— the value to compare with
Returns
- a mask, true in each lane less than
other
less_equal
less_equal(other: Vector<T, N>): Mask<N>
Tests whether each lane is less than or equal to the same lane of
other.
Parameters
other— the vector to compare with
Returns
- a mask, true in each lane that is less or equal
less_equal
less_equal(other: T): Mask<N>
Tests whether every lane is less than or equal to other.
Parameters
other— the value to compare with
Returns
- a mask, true in each lane less than or equal to
other
greater
greater(other: Vector<T, N>): Mask<N>
Tests whether each lane is greater than the same lane of other.
Parameters
other— the vector to compare with
Returns
- a mask, true in each lane that is greater
greater
greater(other: T): Mask<N>
Tests whether every lane is greater than other.
Parameters
other— the value to compare with
Returns
- a mask, true in each lane greater than
other
greater_equal
greater_equal(other: Vector<T, N>): Mask<N>
Tests whether each lane is greater than or equal to the same lane of
other.
Parameters
other— the vector to compare with
Returns
- a mask, true in each lane that is greater or equal
greater_equal
greater_equal(other: T): Mask<N>
Tests whether every lane is greater than or equal to other.
Parameters
other— the value to compare with
Returns
- a mask, true in each lane greater than or equal to
other
min
min(other: Vector<T, N>): Vector<T, N>
The smaller of each lane and the same lane of other. For floats, a
NaN lane gives the other lane.
Parameters
other— the vector to compare with
Returns
- the lane-wise minimums
max
max(other: Vector<T, N>): Vector<T, N>
The larger of each lane and the same lane of other. For floats, a
NaN lane gives the other lane.
Parameters
other— the vector to compare with
Returns
- the lane-wise maximums
abs
abs(): Vector<T, N>
The absolute value of every lane. For a signed integer T the lowest
value has no positive counterpart and stays itself.
Returns
- the lane-wise absolute values
sum
sum(): T
The sum of the lanes. Integer lanes wrap around on overflow; float lanes are added in lane order.
Returns
- the sum of all lanes
minimum
minimum(): T
The smallest lane.
Returns
- the minimum over all lanes
maximum
maximum(): T
The largest lane.
Returns
- the maximum over all lanes
shuffle
shuffle(indices: Vector<int, N>): Vector<T, N>
Picks lanes by index: lane i of the result is lane indices[i] of
this vector. Each index is taken modulo N, so every index picks a
lane. When the indices are constants, an optimized build compiles this
to one shuffle instruction.
Parameters
indices— the lane to pick for each lane of the result
Returns
- the picked lanes
reverse
reverse(): Vector<T, N>
The lanes in reverse order.
Returns
- a vector whose lane
iis laneN - 1 - iof this one
interleave_low
interleave_low(other: Vector<T, N>): Vector<T, N>
Interleaves the low halves of this vector and other: lanes
this[0], other[0], this[1], other[1], ....
Parameters
other— the vector whose lanes go in the odd lanes
Returns
- the interleaved lanes
interleave_high
interleave_high(other: Vector<T, N>): Vector<T, N>
Interleaves the high halves of this vector and other: lanes
this[N / 2], other[N / 2], this[N / 2 + 1], ....
Parameters
other— the vector whose lanes go in the odd lanes
Returns
- the interleaved lanes
convert
convert<U: Lane>(): Vector<U, N>
Converts every lane to U, as as converts a number: integers
truncate or extend, and floats round toward zero into integers. A
float lane outside U’s range gives U’s nearest value, and NaN
gives 0.
Type parameters
U— the new lane type
Returns
- the converted lanes
reinterpret
reinterpret<U: Lane, M>(): Vector<U, M>
Reads the vector’s bits as M lanes of type U. The two must have
the same size in bytes, or it is a compile-time error.
Type parameters
U— the new lane typeM— the new lane count
Returns
- the same bits as a vector of
U
extend Vector<T, N>
extend Vector<T: IntegerLane, N>
The operations of vectors of integers: checked division, the bitwise operators, shifts and rotates.
Type parameters
T— the integer lane typeN— the lane count
Methods
operator /
/(other: Vector<T, N>): Vector<T, N>
Divides each lane by the same lane of other. Division truncates
toward zero.
Parameters
other— the vector to divide by
Returns
- the lane-wise quotients
Panics
- if a lane of
otheris 0, orTis signed and a lane divides the lowest value by -1
operator /
/(other: T): Vector<T, N>
Divides every lane by other. Division truncates toward zero.
Parameters
other— the value to divide by
Returns
- the lane-wise quotients
Panics
- if
otheris 0, orTis signed and a lane divides the lowest value by -1
operator %
%(other: Vector<T, N>): Vector<T, N>
The remainder of dividing each lane by the same lane of other, with
the sign of this vector’s lane.
Parameters
other— the vector to divide by
Returns
- the lane-wise remainders
Panics
- if a lane of
otheris 0, orTis signed and a lane divides the lowest value by -1
operator %
%(other: T): Vector<T, N>
The remainder of dividing every lane by other, with the sign of the
lane.
Parameters
other— the value to divide by
Returns
- the lane-wise remainders
Panics
- if
otheris 0, orTis signed and a lane divides the lowest value by -1
operator &
&(other: Vector<T, N>): Vector<T, N>
The bitwise AND of each lane with the same lane of other.
Parameters
other— the vector to combine with
Returns
- the lane-wise ANDs
operator &
&(other: T): Vector<T, N>
The bitwise AND of every lane with other.
Parameters
other— the value to combine with
Returns
- the lane-wise ANDs
operator |
|(other: Vector<T, N>): Vector<T, N>
The bitwise OR of each lane with the same lane of other.
Parameters
other— the vector to combine with
Returns
- the lane-wise ORs
operator |
|(other: T): Vector<T, N>
The bitwise OR of every lane with other.
Parameters
other— the value to combine with
Returns
- the lane-wise ORs
operator ^
^(other: Vector<T, N>): Vector<T, N>
The bitwise XOR of each lane with the same lane of other.
Parameters
other— the vector to combine with
Returns
- the lane-wise XORs
operator ^
^(other: T): Vector<T, N>
The bitwise XOR of every lane with other.
Parameters
other— the value to combine with
Returns
- the lane-wise XORs
operator <<
<<(count: int): Vector<T, N>
Shifts every lane left by count bits.
Parameters
count— the number of bits to shift by
Returns
- the shifted lanes
Panics
- if
countis negative or not less than the lane’s width in bits
operator >>
>>(count: int): Vector<T, N>
Shifts every lane right by count bits: arithmetic (copying the sign
bit) for a signed T, logical (filling with zeros) for an unsigned one.
Parameters
count— the number of bits to shift by
Returns
- the shifted lanes
Panics
- if
countis negative or not less than the lane’s width in bits
operator ~
~(): Vector<T, N>
Flips every bit of every lane.
Returns
- the complemented lanes
rotate_left
rotate_left(count: int): Vector<T, N>
Rotates the bits of every lane left by count, taken modulo the
lane’s width (a negative count rotates right).
Parameters
count— the number of bits to rotate by
Returns
- the rotated lanes
rotate_right
rotate_right(count: int): Vector<T, N>
Rotates the bits of every lane right by count, taken modulo the
lane’s width (a negative count rotates left).
Parameters
count— the number of bits to rotate by
Returns
- the rotated lanes
extend Vector<T, N>
extend Vector<T: FloatLane, N>
The operations of vectors of floating-point numbers: division and rounding.
Type parameters
T— the floating-point lane typeN— the lane count
Methods
operator /
/(other: Vector<T, N>): Vector<T, N>
Divides each lane by the same lane of other, as floating-point
division does: a zero divisor gives an infinity or NaN.
Parameters
other— the vector to divide by
Returns
- the lane-wise quotients
operator /
/(other: T): Vector<T, N>
Divides every lane by other, as floating-point division does.
Parameters
other— the value to divide by
Returns
- the lane-wise quotients
operator %
%(other: Vector<T, N>): Vector<T, N>
The remainder of dividing each lane by the same lane of other
(C’s fmod), with the sign of this vector’s lane.
Parameters
other— the vector to divide by
Returns
- the lane-wise remainders
operator %
%(other: T): Vector<T, N>
The remainder of dividing every lane by other (C’s fmod).
Parameters
other— the value to divide by
Returns
- the lane-wise remainders
square_root
square_root(): Vector<T, N>
The square root of every lane (NaN for a negative lane).
Returns
- the lane-wise square roots
floor
floor(): Vector<T, N>
Every lane rounded down to a whole number.
Returns
- the lane-wise floors
ceiling
ceiling(): Vector<T, N>
Every lane rounded up to a whole number.
Returns
- the lane-wise ceilings
Mask
[Value] struct Mask<N>
N booleans, one per vector lane: what lane-wise comparisons give.
Type parameters
N— the lane count: a power of two from 1 to 64
Properties
count
var count: int { get; }
The number of lanes, N.
Constructors
Mask()
construct()
Makes a mask with every lane false.
Mask()
construct(value: bool)
Makes a mask with every lane value.
Parameters
value— the value of every lane
Methods
operator []
[](index: ulong): bool
Returns lane index.
Parameters
index— the lane to read
Returns
- the value in lane
index
Panics
- if
indexis not less thanN
any
any(): bool
Whether any lane is true.
Returns
- true when at least one lane is true
all
all(): bool
Whether every lane is true.
Returns
- true when no lane is false
to_bits
to_bits(): ulong
The lanes packed into bits: bit i is lane i. Its
trailing_zeros() is the first true lane (64 when none is), and its
count_ones() the number of true lanes.
Returns
- the lanes as a bit mask
operator &
&(other: Mask<N>): Mask<N>
The lane-wise AND of the two masks.
Parameters
other— the mask to combine with
Returns
- true in each lane true in both
operator |
|(other: Mask<N>): Mask<N>
The lane-wise OR of the two masks.
Parameters
other— the mask to combine with
Returns
- true in each lane true in either
operator ^
^(other: Mask<N>): Mask<N>
The lane-wise XOR of the two masks.
Parameters
other— the mask to combine with
Returns
- true in each lane true in exactly one
operator ~
~(): Mask<N>
The lane-wise negation of the mask.
Returns
- true in each lane that is false here
select
select<T: Lane>(if_true: Vector<T, N>, if_false: Vector<T, N>): Vector<T, N>
Picks each lane from if_true where this mask is true, and from
if_false where it is false.
Type parameters
T— the lane type of the vectors
Parameters
if_true— the lanes to take where the mask is trueif_false— the lanes to take where the mask is false
Returns
- the selected lanes
The debug module
Stack walking, panic backtraces, and talking to a debugger.
debug is a module: bring it in with import debug (and write
debug.capture()).
- Backtraces:
capture()records the calls that led to the current point, cheaply;Backtrace.frames()resolves each return address to its function and, in a-gbuild, its source line.print_backtrace()writes the current stack to standard error. - The debugger:
output()writes to the platform’s debugger channel,is_debugger_attached()asks whether a debugger is watching, andbreakpoint()stops in it.
A panic prints a backtrace after its message in a build without -O;
the PEPPER_BACKTRACE environment variable turns that on (1) or off
(0) in any build (see print_backtrace).
Example
import debug;
main(): int {
var trace = debug.capture();
var frames = trace.frames();
assert(frames.count > 0, "main is on the stack");
debug.output("captured a backtrace");
return 0;
}
Source: runtime/debug/module.pep
Backtraces
Capturing the call stack (capture, Backtrace) and resolving it to
functions and source lines (StackFrame).
Capturing only records return addresses, so it is cheap enough to do
whenever something might need reporting later. The addresses are
resolved, from the program’s symbol table and its debug info, only when
Backtrace.frames() or to_string() asks for them.
A function name needs the program’s symbol table, which every build
keeps unless the executable is stripped. A file, line and column need
the debug info a -g build carries:
- Linux: the executable’s ELF symbol table and DWARF line table.
- macOS: the executable’s symbol table, and the DWARF line table of
the
.dSYMbundle a-gbuild writes beside it. - Windows: DbgHelp, which reads the
.pdbevery build writes beside the executable: function names only, or with-gsource lines too (no columns). - WASI: a WebAssembly program cannot walk its stack, so a backtrace is empty.
A function the optimizer inlined into its caller has no frame of its
own, so each address is expanded into its chain of inlined calls, from
the debug info of a -g build: one StackFrame per call, marked
inlined.
Frames inside the runtime’s own capture and panic code are left out, and
so are the runtime’s operator methods (Array<int>.__index_get, what
a[i] calls) between a panic and the code that caused it, and the C
runtime’s frames below main. frames(full = true), or
PEPPER_BACKTRACE=full for a panic’s backtrace, keeps them all.
Source: runtime/debug/backtrace.pep
StackFrame
struct StackFrame
One call on the stack: where it returns to, and the function and source position that address belongs to.
A call the optimizer inlined is a frame too, with inlined set: it
shares its address with the frames of its callers, up to the first
frame that is not inlined, which is the real one.
Example
import debug;
main(): int {
var frame = debug.capture().frames()[0];
assert(frame.function == "main", "the innermost frame is main");
print(frame.to_string()); # main at main.pep:4:21 in a -g build
return 0;
}
Fields
address
var address: ulong
The return address: the instruction the call returns to.
function
var function: String
The function’s name, such as main or Point.length; empty when the
program has no symbol for the address.
file
var file: String
The source file, as it was named to the compiler; empty without debug info.
line
var line: int
The 1-based source line of the call; 0 without debug info.
column
var column: int
The 1-based source column of the call; 0 when unknown.
inlined
var inlined: bool
Whether the optimizer inlined this call into its caller, the next frame: the function has no frame of its own on the stack.
Constructor
StackFrame()
construct(address: ulong = 0)
Creates a StackFrame for address, with no function or source
position yet.
Parameters
address— the return address
Methods
to_string
to_string(): String
Returns the frame as text: function at file:line:column, with the
parts that are unknown left out, the address in hexadecimal when
the function is unknown, and (inlined) after an inlined call.
Returns
- the frame as text
Backtrace
struct Backtrace
The call stack at the point capture() ran, as return addresses,
innermost first.
Example
import debug;
main(): int {
var trace = debug.capture();
for (var frame in trace.frames()) {
print(frame.to_string());
}
return 0;
}
Fields
addresses
var addresses: Array<ulong>
The return address of each call, innermost first, as captured: it
includes the frames of capture itself, which frames() leaves out.
Constructor
Backtrace()
construct()
Creates an empty Backtrace. Use capture() to record the stack.
Methods
frames
frames(full: bool = false): Array<StackFrame>
Resolves each return address to its function and source position. Runs in time proportional to the program’s symbol table and debug info, which it reads each call.
In a -g build, an address inside code the optimizer inlined
becomes a frame for each inlined call, innermost first, each marked
inlined, followed by the real frame (Linux and macOS read the
DWARF, Windows asks DbgHelp).
Unless full, leaves out, at the top, the frames of capture and of
the runtime’s panic code and the runtime’s operator methods
(Array<int>.__index_get for a[i]) a panic came through; and at
the bottom, the C runtime’s frames below main.
Parameters
full— whether to keep every frame
Returns
- the frames, innermost first
to_string
to_string(): String
Returns the frames as text, one per line, each numbered and indented
( 0: main at main.pep:4:5), with a newline after the last.
Returns
- the frames as text
capture
capture(): Backtrace
Records the call stack: the return address of every call that led here.
Cheap: the addresses are resolved only when Backtrace.frames() or
to_string() asks for them.
Records at most 128 calls, the innermost ones. On WASI the Backtrace is empty.
Returns
- the stack, innermost call first
Example
import debug;
main(): int {
var trace = debug.capture();
print(trace.to_string());
return 0;
}
print_backtrace
print_backtrace()
Writes the current call stack to standard error, a line per frame, after
a backtrace: line: what a panic prints after its message.
A panic prints it in a build without -O. Set the PEPPER_BACKTRACE
environment variable to 1 to print it in an optimized build too, to
full to also print the runtime frames it leaves out (see
Backtrace.frames), or to 0 to never print it.
Example
import debug;
main(): int {
debug.print_backtrace();
return 0;
}
The debugger
Talking to a debugger: output() writes to the platform’s debugger
channel, is_debugger_attached() asks whether a debugger is watching,
and breakpoint() stops in it.
Source: runtime/debug/debugger.pep
output
output(text: String)
Writes text to the platform’s debugger channel, followed by a newline
when it does not end in one.
- Windows:
OutputDebugStringW, which Visual Studio’s Output window and WinDbg show (and DebugView, with no debugger attached). Nothing is written anywhere else. - Linux, macOS and WASI: standard error, which debuggers show; these systems have no separate channel.
Parameters
text— the text to write
Example
import debug;
main(): int {
debug.output("starting");
return 0;
}
is_debugger_attached
is_debugger_attached(): bool
Returns whether a debugger is attached to the program: the TracerPid
of /proc/self/status on Linux, P_TRACED on macOS, and
IsDebuggerPresent on Windows. Always false on WASI.
Returns
- true while a debugger is attached
Example
import debug;
main(): int {
if (debug.is_debugger_attached()) {
debug.output("hello, debugger");
}
return 0;
}
breakpoint
breakpoint()
Stops in the debugger, as a breakpoint set on this line would: the
processor’s debug trap (int3 on x64, brk on AArch64), through
llvm.debugtrap. The debugger can then continue the program.
With no debugger attached the trap ends the program: SIGTRAP on Linux
and macOS, a breakpoint exception on Windows. Guard it with
is_debugger_attached() to stop only under a debugger. Does nothing on
WASI.
Example
import debug;
main(): int {
if (debug.is_debugger_attached()) {
debug.breakpoint();
}
return 0;
}
The log module
Leveled, structured logging: lines with a level, a message, key/value
fields and the place of the call, written whole to standard error or
to a Writer of your choice, from any thread or task.
log is a module: bring it in with import log (and write
log.info("started")).
error,warning,info,debugandtracewrite a line at theirLevel. Each takes optionalfields(made withfield) and atarget, the part of the program the line is about.set_levelpicks the least important level shown, for every target or for one;enabledasks whether a line would be written.set_outputsends lines to aWriter(a file, a socket) in aFormat: readable text or JSON Lines.
By default lines go to standard error as text, in color when it is a
terminal (unless NO_COLOR is set), and info and above are shown. The
PEPPER_LOG environment variable sets the levels when the program first
logs: comma-separated items, each a level for every target or
target=level for one, such as PEPPER_LOG=warning,http=debug. The
levels are off, error, warning (or warn), info, debug and
trace.
A text line reads:
2026-10-17T23:40:01.123Z INFO [http] request done status=200 path="/" (main.pep:5:5)
Example
import log;
main(): int {
log.info("started");
log.warning("disk almost full", [log.field("free_mb", 120)], target = "storage");
log.debug("not shown unless PEPPER_LOG=debug");
return 0;
}
Source: runtime/log/module.pep
Logging
Levels, fields, the output, and the functions that write a line.
Source: runtime/log/log.pep
Level
enum Level
How much a line matters, most important first.
As the level a target shows (set_level, PEPPER_LOG), each level also
shows the ones above it: Info shows errors, warnings and info lines.
Cases
Off
Off
Nothing: as the level a target shows, turns its lines off.
Error
Error
Something failed (error).
Warning
Warning
Something looks wrong but the program carries on (warning).
Info
Info
What the program is doing, at the level of whole operations (info).
Debug
Debug
Detail for finding a bug (debug).
Trace
Trace
Step-by-step detail (trace).
Format
enum Format
How each line is written.
Cases
Text
Text
One readable line: the UTC time, the level, the target in brackets,
the message, key=value fields, and the call’s location in
parentheses.
Json
Json
One JSON object per line (JSON Lines), with the keys time,
level, target (when there is one), message and location, then
each field.
Field
struct Field
A key and value attached to a line. Make one with field.
Fields
key
var key: String = ""
The key.
value
var value: String = ""
The value as JSON text: 200, "ann", true.
Constructor
Field()
construct(key: String, value: String)
Creates a field from a key and the JSON text of its value; field
makes the text from any value.
Parameters
key— the keyvalue— the value’s JSON text
field
field<T>(key: String, value: T): Field
Makes a field for a line: log.info("done", [log.field("ms", 12)]).
The value is turned into JSON (see json.encode) when the field is
made, whether or not the line is written; guard costly fields with
enabled.
Type parameters
T— the type ofvalue
Parameters
key— the keyvalue— the value
Returns
- the field
Example
import log;
main(): int {
log.info("request done", [log.field("status", 200), log.field("path", "/")]);
return 0;
}
set_level
set_level(level: Level, target: String = "")
Sets the level a target shows, or with no target the level every target
without its own shows. The default is Level.Info, unless the
PEPPER_LOG environment variable says otherwise.
Parameters
level— the least important level to show;Level.Offshows nothingtarget— the target, as the logging functions’target; empty for the global level
Example
import log;
main(): int {
log.set_level(log.Level.Debug);
log.set_level(log.Level.Warning, "http");
log.debug("shown");
log.info("not shown", target = "http");
return 0;
}
set_output
set_output([Move] writer: io.Writer, format: Format = Format.Text)
Sends every line from now on to writer in format, instead of to
standard error. Lines are written whole, one write_all each, under a
lock, so lines from many threads and tasks never interleave. A failed
write is ignored. Colors are off.
Parameters
writer— where lines go: a new value, or one handed over withmove(x), since the log keeps itformat— how each line is written
Example
import io;
import log;
main(): int {
var path = io.Directory.temp().join("app.log");
var file = io.File.create(path).value();
log.set_output(move(file), log.Format.Json);
log.info("started");
return 0;
}
enabled
enabled(level: Level, target: String = ""): bool
Reports whether a line at level for target would be written, so a
caller can skip building a costly message or field.
Cheap when no target shows level: two atomic loads, with no lock.
Parameters
level— the line’s leveltarget— the line’s target; empty for none
Returns
- true when
targetshowslevel; false forLevel.Off
Example
import log;
main(): int {
if (log.enabled(log.Level.Debug)) {
log.debug($"state: {[1, 2, 3]}");
}
return 0;
}
error
error( message: String, fields: Array<Field> = Array<Field>(), target: String = "" )
Writes an error line: something failed.
Parameters
message— the messagefields— keys and values to add to the line (seefield)target— the part of the program the line is about, whichset_levelandPEPPER_LOGcan show or hide on its own; empty for none
Example
log.error("cannot open the database", [log.field("path", path)]);
warning
warning( message: String, fields: Array<Field> = Array<Field>(), target: String = "" )
Writes a warning line: something looks wrong but the program carries on.
Parameters
message— the messagefields— keys and values to add to the line (seefield)target— the part of the program the line is about; empty for none
Example
log.warning("retrying", [log.field("attempt", 2)], target = "http");
info
info( message: String, fields: Array<Field> = Array<Field>(), target: String = "" )
Writes an info line: what the program is doing.
Parameters
message— the messagefields— keys and values to add to the line (seefield)target— the part of the program the line is about; empty for none
Example
log.info("listening", [log.field("port", 8080)]);
debug
debug( message: String, fields: Array<Field> = Array<Field>(), target: String = "" )
Writes a debug line: detail for finding a bug. Not shown by default.
Parameters
message— the messagefields— keys and values to add to the line (seefield)target— the part of the program the line is about; empty for none
Example
log.debug("cache miss", [log.field("key", key)], target = "cache");
trace
trace( message: String, fields: Array<Field> = Array<Field>(), target: String = "" )
Writes a trace line: step-by-step detail. Not shown by default.
Parameters
message— the messagefields— keys and values to add to the line (seefield)target— the part of the program the line is about; empty for none
Example
log.trace("read a frame", [log.field("bytes", 512)], target = "http");
The text module
Converting text to and from bytes in a given encoding.
Bring it in with import text. A String always holds UTF-8; these
types encode one into the bytes of an encoding (u8[]) and decode bytes
back. Each decode is strict and gives none when the bytes are not valid
in the encoding (code page 437’s always succeeds, since every byte is a
character); each decode_lossy replaces what is invalid with U+FFFD
and always succeeds.
- UTF-8:
Utf8 - UTF-16:
Utf16 - ASCII:
Ascii - Code page 437:
CodePage437
Example
import text;
main(): int {
var bytes = text.Utf8.encode("héllo");
assert(bytes.count == 6, "é takes two bytes");
assert(text.Utf8.decode(bytes).value() == "héllo", "round trip");
return 0;
}
Source: runtime/text/module.pep
UTF-8
UTF-8, the encoding a String holds.
Source: runtime/text/utf8.pep
Utf8
struct Utf8
Encodes a String to UTF-8 bytes and decodes them back. A String already
holds UTF-8, so encoding copies its bytes; decoding checks them.
Example
import text;
var bytes = text.Utf8.encode("hi");
assert(text.Utf8.decode(bytes).value() == "hi", "round trip");
Static methods
encode
static encode(value: String): u8[]
Returns the UTF-8 bytes of value, without a terminating NUL.
Parameters
value— the text to encode
Returns
- a new array of
value.sizebytes
is_valid
static is_valid(bytes: u8[]): bool
Returns whether bytes is valid UTF-8: no stray continuation bytes,
no truncated, overlong or surrogate sequences, and nothing past
U+10FFFF.
Parameters
bytes— the bytes to check
Returns
- true when every byte is part of a valid sequence
decode
static decode(bytes: u8[]): String?
Decodes bytes as UTF-8, or gives none when they are not valid UTF-8
(see is_valid). A byte-order mark is kept as part of the text.
Parameters
bytes— the bytes to decode
Returns
- the text, or none when
bytesis not valid UTF-8
Example
import text;
var bad = Array<u8>();
bad.append(255);
assert(text.Utf8.decode(bad).is_empty(), "0xFF is never UTF-8");
decode_lossy
static decode_lossy(bytes: u8[]): String
Decodes bytes as UTF-8, replacing each byte that does not start a
valid sequence with U+FFFD, so it always succeeds. Valid input gives
the same text as decode.
Parameters
bytes— the bytes to decode
Returns
- the text
Example
import text;
var data = Array<u8>();
data.append(104);
data.append(255);
data.append(105);
assert(text.Utf8.decode_lossy(data) == "h\u{FFFD}i", "255 is replaced");
find_invalid
static find_invalid(value: String): ulong?
Returns the byte offset of the first byte of value that does not
start a valid UTF-8 sequence (see is_valid), or none when all of it
is valid. A String made from checked bytes is always valid; one made
from C memory may not be.
Parameters
value— the text to check
Returns
- the offset of the first invalid byte, or none
Example
import text;
assert(text.Utf8.find_invalid("héllo").is_empty(), "valid");
code_point_at
static code_point_at(value: String, offset: ulong): (uint, ulong)
Decodes the code point whose UTF-8 sequence starts at byte offset
of value.
Parameters
value— the text to readoffset— the byte offset where the sequence starts
Returns
- the code point and the number of bytes its sequence takes, from 1 to 4, so
offsetplus that is where the next one starts
Panics
- if
offsetis not less thanvalue.size(“Utf8.code_point_at offset out of bounds”) - if no valid sequence starts at
offset(“Utf8.code_point_at found invalid UTF-8”)
Example
import text;
var (point, run) = text.Utf8.code_point_at("é!", 0);
assert(point == 233 and run == 2, "U+00E9 takes two bytes");
append_code_point
static append_code_point(out: u8[], code_point: uint)
Appends the UTF-8 encoding of code_point, from 1 to 4 bytes, to
out.
Parameters
out— the bytes to append tocode_point— the code point to encode
Panics
- if
code_pointis a surrogate (U+D800 to U+DFFF) or above U+10FFFF (“Utf8.append_code_point needs a Unicode scalar value”)
Example
import text;
var out = Array<u8>();
text.Utf8.append_code_point(out, 8364);
assert(text.Utf8.decode(out).value() == "€", "U+20AC");
UTF-16
UTF-16, as Windows and Java hold text.
Source: runtime/text/utf16.pep
Utf16
struct Utf16
Encodes a String to UTF-16 bytes and decodes them back. Each code
unit takes two bytes, little-endian unless big_endian is set; a code
point past U+FFFF takes two units (a surrogate pair). No byte-order mark
is written, and one read is kept as part of the text.
Example
import text;
var bytes = text.Utf16.encode("hi");
assert(bytes.count == 4 && bytes[0] == 104 && bytes[1] == 0, "little-endian");
assert(text.Utf16.decode(bytes).value() == "hi", "round trip");
Static methods
encode
static encode(value: String, big_endian: bool = false): u8[]
Returns the UTF-16 bytes of value.
Parameters
value— the text to encodebig_endian— whether each code unit’s high byte comes first; false (little-endian) by default
Returns
- a new array of two bytes per code unit
decode
static decode(bytes: u8[], big_endian: bool = false): String?
Decodes UTF-16 bytes, or gives none when they are not valid UTF-16:
an odd number of bytes, or a surrogate without its partner.
Parameters
bytes— the bytes to decodebig_endian— whether each code unit’s high byte comes first; false (little-endian) by default
Returns
- the text, or none when
bytesis not valid UTF-16
decode_lossy
static decode_lossy(bytes: u8[], big_endian: bool = false): String
Decodes UTF-16 bytes, replacing each unpaired surrogate, and a
final odd byte, with U+FFFD, so it always succeeds.
Parameters
bytes— the bytes to decodebig_endian— whether each code unit’s high byte comes first; false (little-endian) by default
Returns
- the text
ASCII
ASCII, the first 128 code points, one byte each.
Source: runtime/text/ascii.pep
Ascii
struct Ascii
Encodes a String to ASCII bytes and decodes them back. Only code
points below 128 have an ASCII byte.
Example
import text;
assert(text.Ascii.encode("ok").value().count == 2, "one byte each");
assert(text.Ascii.encode("é").is_empty(), "not ASCII");
Static methods
encode
static encode(value: String): u8[]?
Returns the ASCII bytes of value, or none when it holds a code
point past 127.
Parameters
value— the text to encode
Returns
- a new array of one byte per code point, or none
encode_lossy
static encode_lossy(value: String): u8[]
Returns the ASCII bytes of value, with ? for each code point past
127.
Parameters
value— the text to encode
Returns
- a new array of one byte per code point
is_ascii
static is_ascii(value: String): bool
Returns whether every code point of value is ASCII (below 128).
Parameters
value— the text to check
Returns
- true when
valueis all ASCII
decode
static decode(bytes: u8[]): String?
Decodes ASCII bytes, or gives none when one is 128 or more.
Parameters
bytes— the bytes to decode
Returns
- the text, or none when
bytesis not all ASCII
decode_lossy
static decode_lossy(bytes: u8[]): String
Decodes ASCII bytes, replacing each byte of 128 or more with
U+FFFD, so it always succeeds.
Parameters
bytes— the bytes to decode
Returns
- the text
Code page 437
Code page 437, the character set of the original IBM PC, which old zip archives use for entry names.
Source: runtime/text/code_page_437.pep
CodePage437
struct CodePage437
Encodes a String to code page 437 bytes and decodes them back. Bytes
below 128 are ASCII; each of the 128 above stands for one character
(accented letters, Greek letters, box-drawing and math symbols), so
every byte decodes and decode always succeeds. Bytes below 32 decode
as the ASCII control characters, not the PC’s graphical glyphs, as zip
readers do.
Example
import text;
var bytes = text.CodePage437.encode("café").value();
assert(bytes.count == 4 && bytes[3] == 0x82, "é is 0x82");
assert(text.CodePage437.decode(bytes) == "café", "round trip");
Static methods
encode
static encode(value: String): u8[]?
Returns the code page 437 bytes of value, or none when it holds a
character the code page lacks.
Parameters
value— the text to encode
Returns
- a new array of one byte per code point, or none
encode_lossy
static encode_lossy(value: String): u8[]
Returns the code page 437 bytes of value, with ? for each
character the code page lacks.
Parameters
value— the text to encode
Returns
- a new array of one byte per code point
decode
static decode(bytes: u8[]): String
Decodes code page 437 bytes. Every byte is a character, so it
always succeeds.
Parameters
bytes— the bytes to decode
Returns
- the text
The regex module
Regular expressions: test whether text matches a pattern, find the matches and their capture groups, and replace or split by a pattern.
Bring it in with import regex. The syntax is RE2’s, and matching takes
time linear in the size of the text whatever the pattern.
- Regex and Match:
Regex,Match,Matches
Example
import regex;
main(): int {
var word = regex.Regex("\\b(\\w)(\\w*)\\b");
assert(word.replace_all("hello big world", "${1}.") == "h. b. w.", "initials");
return 0;
}
Source: runtime/regex/module.pep
Regex and Match
Regex, Match and the iterator over every match.
Source: runtime/regex/regex.pep
Regex
[Shared] struct Regex
A compiled regular expression: compile a pattern once, then test, find, replace or split text with it.
The syntax is RE2’s, the one Go and Rust’s regex use. Matching runs in
time linear in the text whatever the pattern, because the engine never
backtracks; in return there are no backreferences (\1) and no
lookaround ((?=...)), and a pattern using them fails to compile.
- Positions (
Match.start(),Match.end(),find_at) are byte offsets, likeString.find, so they work withsubstr_bytes. .and a negated class ([^a]) match one code point, never part of one.\d,\w,\s,\band case-insensitive matching are ASCII-only.
A Regex never changes once compiled and is [Shared], so one compiled at
module scope (const word = regex.Regex("\\w+");) serves every thread.
Example
import regex;
main(): int {
var date = regex.Regex("(\\d{4})-(\\d{2})-(\\d{2})");
var found = date.find("due 2026-10-01").value();
assert(found.group(1).value() == "2026", "the year");
assert(found.start() == 4, "a byte offset");
return 0;
}
Constructors
Regex()
construct(pattern: String)
Compiles pattern.
Parameters
pattern— the regular expression
Panics
- if
patternis not a valid regular expression; useparsefor a pattern that may be invalid, such as one from user input
Regex()
construct(program: _RegexProgram)
Wraps a compiled program. Use Regex(pattern) or Regex.parse; this
constructor is for the runtime.
Parameters
program— the compiled pattern
Static methods
parse
static parse(pattern: String): Result<Regex>
Compiles pattern, or reports why it is invalid.
Parameters
pattern— the regular expression
Returns
- the compiled expression, or an error naming the problem and its byte offset in
pattern, such asinvalid regex '(a': missing ')' at offset 0
Example
import regex;
assert(regex.Regex.parse("[a-z]+").is_ok(), "valid");
assert(regex.Regex.parse("(a").is_err(), "unclosed group");
escape
static escape(text: String): String
Returns text with every character that has a meaning in a pattern
escaped, so the result matches text literally.
Parameters
text— the text to match
Returns
- a pattern that matches exactly
text
Example
import regex;
assert(regex.Regex.escape("1+1=2?") == "1\\+1=2\\?", "escaped");
Methods
pattern
pattern(): String
Returns the pattern this expression was compiled from.
Returns
- the pattern text
group_count
group_count(): ulong
Returns the number of capture groups in the pattern, not counting the whole match.
Returns
- the number of
(...)groups, named ones included
is_match
is_match(text: String): bool
Returns whether the expression matches anywhere in text. Faster
than find, since it stops at the first match it sees.
Parameters
text— the text to search
Returns
- true when there is a match
Example
import regex;
assert(regex.Regex("^\\d+$").is_match("2026"), "all digits");
find
find(text: String): Match?
Returns the leftmost match in text, or none. Among matches that
start at the same place, a greedy repetition prefers the longer one
and alternation the earlier branch, as in Perl.
Parameters
text— the text to search
Returns
- the first match, or none
find_at
find_at(text: String, start: ulong): Match?
Returns the leftmost match in text that starts at or after byte
offset start, or none. Assertions still see the text before start:
^ does not match at start unless it begins a line, and \b looks
at the character before it.
Parameters
text— the text to searchstart— the byte offset to search from, at mosttext.size
Returns
- the first match from
start, or none
Panics
- if
startis greater thantext.size
find_all
find_all(text: String): Matches
Returns an iterator over the matches in text, left to right and
without overlaps. An empty match right after the previous match is
skipped, so a* finds a twice in "aba", not also between them.
Parameters
text— the text to search
Returns
- an iterator over the matches
Example
import regex;
var total = 0;
for (var number in regex.Regex("\\d+").find_all("1 22 333")) {
total += number.text().size; # 1, then 2, then 3
}
replace
replace(text: String, replacement: String): String
Returns text with the first match replaced by replacement, or a
copy of text when nothing matches.
In replacement, $1 or ${1} stands for group 1’s text (empty when
the group did not take part), ${name} for a named group’s, $0 for
the whole match, and $$ for a $. A $ followed by anything else
is kept as it is.
Parameters
text— the text to searchreplacement— the text to put in place of the match
Returns
- a new String
Panics
- if
replacementnames a group the pattern does not have
Example
import regex;
var swapped = regex.Regex("(\\w+) (\\w+)").replace("hello world", "$2 $1");
assert(swapped == "world hello", "groups swapped");
replace_all
replace_all(text: String, replacement: String): String
Returns text with every match, as find_all finds them, replaced by
replacement, which may name groups as in replace.
Parameters
text— the text to searchreplacement— the text to put in place of each match
Returns
- a new String
Panics
- if
replacementnames a group the pattern does not have
Example
import regex;
var squeezed = regex.Regex("\\s+").replace_all("a b\t\nc", " ");
assert(squeezed == "a b c", "runs of space replaced");
split
split(text: String): String[]
Splits text at each match, as find_all finds them, and returns the
pieces between them. A match at the start or end gives an empty first
or last piece, and text with no match gives one piece, the whole text.
Parameters
text— the text to split
Returns
- the pieces, at least one
Example
import regex;
var parts = regex.Regex(",\\s*").split("a, b,c");
assert(parts.count == 3 && parts[1] == "b", "three pieces");
to_string
to_string(): String
Returns the pattern, so print and interpolation show it.
Returns
- the pattern text
Match
struct Match
One match of a Regex: where it is in the text, and what each capture
group matched.
Positions are byte offsets into the searched text. Group 0 is the whole
match; groups are numbered from 1 by their opening (, named ones
included.
Constructor
Match()
construct(text: String, slots: long[], program: _RegexProgram)
Creates a match over text from its capture slots. Matches come from
Regex; this constructor is for the runtime.
Parameters
text— the searched textslots— a start and end byte offset for the match and each group, -1 for a group that did not take partprogram— the compiled pattern, for its group names
Methods
text
text(): String
Returns the matched text.
Returns
- the text of the whole match
start
start(): ulong
Returns the byte offset where the match starts.
Returns
- the offset of the match’s first byte
end
end(): ulong
Returns the byte offset just past the end of the match.
Returns
- the offset after the match’s last byte; equal to
start()for an empty match
group_count
group_count(): ulong
Returns the number of capture groups, not counting the whole match.
Returns
- the number of groups in the pattern
group
group(index: ulong): String?
Returns the text that group index matched, or none when the group
did not take part in the match (as in (a)|b matching b).
Parameters
index— the group number; 0 is the whole match
Returns
- the group’s text, or none
Panics
- if
indexis greater thangroup_count()
group
group(name: String): String?
Returns the text that the group named name ((?<name>...) or
(?P<name>...)) matched, or none when it did not take part.
Parameters
name— the group’s name
Returns
- the group’s text, or none
Panics
- if the pattern has no group named
name
group_range
group_range(index: ulong): (ulong, ulong)?
Returns the byte offsets where group index starts and ends, or none
when it did not take part.
Parameters
index— the group number; 0 is the whole match
Returns
- the start and end offsets, or none
Panics
- if
indexis greater thangroup_count()
to_string
to_string(): String
Returns the matched text.
Returns
- the text of the whole match
Matches
struct Matches
An iterator over the matches of a Regex in a text, from
Regex.find_all: left to right, without overlaps.
Constructor
Matches()
construct(program: _RegexProgram, text: String)
Creates an iterator over the matches of program in text. Use
Regex.find_all; this constructor is for the runtime.
Parameters
program— the compiled patterntext— the text to search
Methods
next
next(): bool
Advances to the next match.
Returns
- true when there is another match, false once they are exhausted
current
current(): Match
Returns the match the last successful next() moved to.
Returns
- the current match
Panics
- if
next()has not returned true
The json module
JSON: parse text into a JsonValue, look values up and change them, and
write them back as compact or indented text.
Bring it in with import json. Parsing follows RFC 8259 strictly, and
accepts JSONC (comments and trailing commas) on request. Objects keep
their keys in the order they were written, so a document round-trips
unchanged. encode and decode convert structs to JSON text and back.
- JsonValue:
JsonValue - Parsing:
parse,JsonError - Documents with comments:
parse_document,JsonDocument - Struct conversion:
encode,decode,to_json,from_json,UnknownKeys, with the attributes of the serialize module
Example
import json;
main(): int {
var parsed = json.parse("{\"name\": \"pepper\", \"port\": 8080}");
if (parsed.is_err()) {
print(parsed.error().to_string());
return 1;
}
var root = parsed.value();
assert(root["name"].as_string().value() == "pepper", "a string");
assert(root["port"].as_int().value_or(80) == 8080, "a number");
root["debug"] = json.JsonValue.Bool(true);
assert(root.to_string() == "{\"name\":\"pepper\",\"port\":8080,\"debug\":true}", "compact");
return 0;
}
Source: runtime/json/module.pep
JsonValue
JsonValue: a JSON value of any kind, and how it is written as text.
Source: runtime/json/value.pep
JsonValue
enum JsonValue
A JSON value: null, a boolean, a number, a string, an array or an
object. json.parse makes one from text and to_string writes it back.
Look values up with value["key"] and value[index]. A lookup that
finds nothing (a missing key, an index past the end, or a lookup on a
value of another kind) gives Null rather than panicking, so lookups
chain: root["server"]["port"]. Then read the value with an as_ method,
which gives none when the value is of another kind, or match on its
case.
Numbers keep the form they were written in: an integer within
±(2^53 − 1), the range every JSON implementation agrees on (RFC 7493), is
an Integer; any other number is a Number, a double.
An Array or Object case holds its array or map by reference, so
changing them through value[key] = ..., append or the map from
as_object() changes every holder of the value.
Example
import json;
var reply = json.JsonValue.object();
reply["id"] = json.JsonValue.Integer(42);
reply["tags"] = json.JsonValue.array([json.JsonValue.String("a")]);
assert(reply.to_string() == "{\"id\":42,\"tags\":[\"a\"]}", "compact");
assert(reply["id"].as_int().value() == 42, "a lookup");
assert(reply["missing"]["deeper"].is_null(), "lookups chain");
Cases
Null
Null
JSON’s null, and what a lookup gives when it finds nothing.
Bool
Bool(value: bool)
true or false.
Parameters
value— the boolean
Integer
Integer(value: long)
A number written without a fraction or an exponent, within ±(2^53 − 1).
Parameters
value— the integer
Number
Number(value: double)
Any other number: one with a fraction or an exponent, or an integer outside ±(2^53 − 1).
Parameters
value— the number
String
String(value: String)
A string.
Parameters
value— the text
Array
Array(items: Array<JsonValue>)
An array, holding its elements in order.
Parameters
items— the elements
Object
Object(members: OrderedMap<String, JsonValue>)
An object, holding its members in insertion order: the order the text or the code wrote them, so a document round-trips unchanged.
Parameters
members— the members, by key
Static methods
object
static object(): JsonValue
Returns a new, empty object.
Returns
- an
Objectwith no members
Example
import json;
var config = json.JsonValue.object();
config["name"] = json.JsonValue.String("pepper");
array
static array(items: Array<JsonValue> = Array<JsonValue>()): JsonValue
Returns an array holding items, which it keeps by reference rather
than copying.
Parameters
items— the elements; an empty array when not given
Returns
- an
Arrayofitems
Example
import json;
var list = json.JsonValue.array([json.JsonValue.Integer(1)]);
list.append(json.JsonValue.Integer(2));
assert(list.to_string() == "[1,2]", "two elements");
Methods
operator []
[](key: String): JsonValue
Reads value[key]: the member named key of an object.
Parameters
key— the member’s key
Returns
- the member, or
Nullwhen this is not an object or has no member namedkey
operator []
[](key: String, member: JsonValue): JsonValue
Writes value[key] = member: sets the member named key of an
object. A new key goes at the end; an existing key keeps its place.
Parameters
key— the member’s keymember— the value to store
Returns
member, the value of the assignment expression
Panics
- if this is not an object (“JsonValue[key] = value needs an object”)
operator []
[](index: ulong): JsonValue
Reads value[index]: an element of an array.
Parameters
index— the element’s position, from 0
Returns
- the element, or
Nullwhen this is not an array orindexis not less than its count
operator []
[](index: ulong, element: JsonValue): JsonValue
Writes value[index] = element: replaces an element of an array.
Parameters
index— the element’s position, from 0element— the value to store
Returns
element, the value of the assignment expression
Panics
- if this is not an array (“JsonValue[index] = value needs an array”)
- if
indexis not less than the array’s count (asArray’s indexer does)
append
append(element: JsonValue)
Appends element to the end of an array.
Parameters
element— the value to append
Panics
- if this is not an array (“JsonValue.append needs an array”)
contains
contains(key: String): bool
Returns whether this is an object with a member named key.
Parameters
key— the key to look for
Returns
- true when the member exists, even if its value is
Null
remove
remove(key: String): bool
Removes the member named key from an object.
Parameters
key— the key to remove
Returns
- true when this is an object and the member was there
is_null
is_null(): bool
Returns whether this is Null, which is also what a lookup that finds
nothing gives.
Returns
- true for
Null
as_bool
as_bool(): bool?
Returns the boolean, when this is one.
Returns
- the value of a
Bool, or none
as_int
as_int(): long?
Returns the number as an integer, when it is one: an Integer, or a
Number with no fraction that fits in a long (2.0, 1e3).
Returns
- the integer, or none
Example
import json;
assert(json.parse("1e3").value().as_int().value() == 1000, "no fraction");
assert(json.parse("1.5").value().as_int().is_empty(), "a fraction");
as_double
as_double(): double?
Returns the number as a double, when this is an Integer or a
Number.
Returns
- the number, or none
as_string
as_string(): String?
Returns the string, when this is one.
Returns
- the text of a
String, or none
as_array
as_array(): Array<JsonValue>?
Returns the elements, when this is an array. The array is the one this value holds, not a copy.
Returns
- the elements of an
Array, or none
as_object
as_object(): OrderedMap<String, JsonValue>?
Returns the members, when this is an object. The map is the one this value holds, not a copy.
Returns
- the members of an
Object, or none
to_string
to_string(): String
Returns the value as compact JSON text, with no spaces or line
breaks: {"id":42,"tags":["a","b"]}.
Strings are written as UTF-8, escaping ", \ and the control
characters. A Number that is not finite (NaN or an infinity, which
JSON cannot spell) is written as null.
Returns
- the JSON text
to_string
to_string(ascii_only: bool): String
Returns the value as compact JSON text, like to_string(), choosing
whether to escape every character outside ASCII as \u (a pair of
them past U+FFFF), so the text is plain ASCII.
Parameters
ascii_only— whether to escape every non-ASCII character
Returns
- the JSON text
Example
import json;
var word = json.JsonValue.String("é");
assert(word.to_string(ascii_only = true) == "\"\\u00e9\"", "escaped");
to_pretty_string
to_pretty_string(indent: int = 2, ascii_only: bool = false): String
Returns the value as indented JSON text: each element and member on
its own line, indented indent spaces per level, with a space after
each :. An empty array or object stays [] or {}.
Parameters
indent— the number of spaces per level of nestingascii_only— whether to escape every non-ASCII character as\u
Returns
- the JSON text, without a final line break
Panics
- if
indentis negative (“JsonValue.to_pretty_string needs a non-negative indent”)
Example
import json;
var list = json.JsonValue.array([json.JsonValue.Integer(1)]);
assert(list.to_pretty_string() == "[\n 1\n]", "two spaces");
Parsing
parse: reading JSON text into a JsonValue, and JsonError, what it
reports when the text is not JSON.
Source: runtime/json/parse.pep
JsonError
struct JsonError
What parse reports when the text is not JSON: what is wrong, and the
line and column where it is.
to_string() renders it like a compiler error:
config.json:3:17: error: expected ',' or '}'.
Fields
message
var message: String
What is wrong, worded like a compiler error (expected ':').
path
var path: String
The path given to parse, which names the text in to_string();
empty when none was given.
line
var line: ulong
The line of the error, from 1; 0 for an error from from_json, which
has no text to point into.
column
var column: ulong
The column of the error, from 1, counting bytes as clang does; 0
when line is.
offset
var offset: ulong
The byte offset of the error in the text, from 0.
value_path
var value_path: String
Where in the document the value an error is about is, as a path from
the root $ ($.server.port); empty for an error in the text’s
syntax. decode and from_json set it, and start message with it.
Constructor
JsonError()
construct(message: String = "", path: String = "", line: ulong = 1, column: ulong = 1, offset: ulong = 0)
Creates an error.
Parameters
message— what is wrongpath— the name of the text, or emptyline— the line of the error, from 1column— the column of the error, from 1offset— the byte offset of the error, from 0
Methods
to_string
to_string(): String
Returns the error as a compiler prints one:
path:line:column: error: message, without the path: when there
is no path and without line:column: when line is 0.
Returns
- the rendered error
parse
parse( text: String, comments: bool = false, path: String = "", max_depth: ulong = 1000 ): Result<JsonValue, JsonError>
Parses JSON text into a JsonValue.
Parsing is strict RFC 8259: one value, surrounded only by spaces, tabs
and line breaks. A byte-order mark at the start is skipped. The text
must be valid UTF-8, and a string cannot hold a raw control character
or a \u escape of an unpaired surrogate ("\uD800"). When an object
repeats a key, the last value wins, in the place of the first.
With comments = true the text may be JSONC, as VS Code’s settings
files are: // line comments, /* */ block comments, and a trailing
comma after the last element or member. The comments are skipped.
Nesting comes from the text, so the parser keeps its own stack rather
than recursing, and max_depth bounds it.
Parameters
text— the JSON textcomments— whether to accept comments and trailing commaspath— the name of the text (a file name, say), which the error’sto_string()puts firstmax_depth— how deeply arrays and objects may nest
Returns
- the value, or an error naming the first problem and its line and column
Example
import json;
var parsed = json.parse("{\"server\": {\"port\": 8080}}");
assert(parsed.value()["server"]["port"].as_int().value() == 8080, "a lookup");
var bad = json.parse("[1 2]", path = "list.json");
assert(bad.error().to_string() == "list.json:1:4: error: expected ',' or ']'", "where");
Documents with comments
JsonDocument: a JSONC document that keeps its comments, so a settings
file can be read, changed and written back with them.
Source: runtime/json/document.pep
JsonDocument
struct JsonDocument
A JSON value with the comments of the JSONC text it was read from, as
parse_document makes it.
Each comment belongs to a value, by the value’s path from the root $
($.editor.tab_size, $.files[0]): the comments on the lines before
a value, or on the line it ends (after its comma), stay with it, and
those before a closing bracket stay at the end of their array or object.
to_string() writes them back in those places, so text read and
written unchanged keeps every comment, in the indented layout
to_pretty_string uses.
Change root freely. A comment whose value is gone (a removed key, an
array shortened past it) is not written; one whose path names another
value afterwards (an element shifted into its index) goes with that
value.
Example
import json;
var text = "{\n // The editor.\n \"tab_size\": 4, // spaces\n \"wrap\": true\n}";
var document = json.parse_document(text).value();
document.root["wrap"] = json.JsonValue.Bool(false);
assert(
document.to_string() == "{\n // The editor.\n \"tab_size\": 4, // spaces\n \"wrap\": false\n}",
"the comments stay"
);
assert(document.comments("$.tab_size")[0] == "// The editor.", "by path");
Fields
root
var root: JsonValue
The document’s value.
Constructor
JsonDocument()
construct(root: JsonValue = JsonValue.Null)
Creates a document holding root, with no comments.
Parameters
root— the document’s value
Methods
comments
comments(path: String): Array<String>
Returns the comments on the lines before the value at path, each
with its // or /* */.
Parameters
path— the value’s path from the root:$,$.key,$[0], or$["a key"]for a key that is not an identifier
Returns
- the comments, oldest first; empty when there are none
set_comments
set_comments(path: String, comments: Array<String>)
Replaces the comments on the lines before the value at path. Each
must be a whole comment, // ... (one line) or /* ... */.
Parameters
path— the value’s path from the root, as forcommentscomments— the comments to write before the value; empty removes them
Panics
- if a comment does not start with
//or/*(“a JSONC comment starts with ‘//’ or ‘/*’”)
to_string
to_string(indent: int = 2, ascii_only: bool = false): String
Returns the document as indented JSONC text with its comments, each
element and member on its own line, as to_pretty_string writes it.
Parameters
indent— the number of spaces per level of nestingascii_only— whether to escape every non-ASCII character as\u
Returns
- the text, without a final line break
Panics
- if
indentis negative (“JsonDocument.to_string needs a non-negative indent”)
parse_document
parse_document(text: String, path: String = "", max_depth: ulong = 1000): Result<JsonDocument, JsonError>
Parses JSONC text into a JsonDocument, keeping its comments.
The text is read as parse reads it with comments = true: // and
/* */ comments, and a trailing comma after the last element or member.
Use it to change a file people edit, such as a settings file, and write
it back with its comments; use parse when the comments do not matter.
Parameters
text— the JSONC textpath— the name of the text (a file name, say), which the error’sto_string()puts firstmax_depth— how deeply arrays and objects may nest
Returns
- the document, or an error naming the first problem and its line and column
Struct conversion
Struct conversion: encode and decode turn Pepper values into JSON text
and back, through reflection, with no per-type code to write.
Source: runtime/json/convert.pep
UnknownKeys
enum UnknownKeys
What decode does with an object key that names no property of the
struct it is read into.
Cases
Ignore
Ignore
Skips the key and its value.
Reject
Reject
Fails with an error naming the key and its path.
to_json
to_json<T>(value: T): JsonValue
Converts value to a JsonValue.
T may be a number type, bool, String, an enum (a case that carries
no values as its name, "Point"; one that carries values as
{"Circle": {"radius": 2}}; [Tag] and [Untagged] choose other forms), Optional<U> (the value, or null), Array<U>, a map with String keys (HashMap, OrderedMap or
Map) as an object, JsonValue itself, or a struct, whose public
properties become an object’s members in declaration order. Element and
property types follow the same rules, so structs nest.
A struct property takes its key from [Name("key")] when it has one,
and is left out when marked [Ignore] (the attributes of the serialize
module, which json exports). A property of a struct type that holds no
object (a struct field never assigned) is written as null. An empty
Array, even one never assigned, is written as [].
The conversion is generated while compiling, by a static block per
type. A type it cannot convert, such as a Result or an io.File, is a
compile error naming the property:
'encode' cannot convert property 'outcome' of type 'Result<int>' to JSON.
Type parameters
T— the type of the value
Parameters
value— the value to convert
Returns
- the value as JSON
Example
import json;
struct Point {
var x: int;
var y: int;
}
main(): int {
var p = Point();
p.x = 1;
p.y = 2;
assert(json.to_json(p)["y"].as_int().value() == 2, "a member");
return 0;
}
encode
encode<T>(value: T, indent: int = 0, ascii_only: bool = false): String
Writes value as JSON text, converting it as to_json does.
Type parameters
T— the type of the value
Parameters
value— the value to writeindent— spaces per nesting level; 0, the default, writes compact text on one lineascii_only— whether to escape every character outside ASCII as\u
Returns
- the JSON text
Example
import json;
struct ServerConfig {
var host: String;
var port: int;
[Name("max-connections")]
var max_connections: int;
[Ignore]
var started_at: long;
}
main(): int {
var config = ServerConfig();
config.host = "localhost";
config.port = 8080;
config.max_connections = 16;
var text = json.encode(config);
assert(text == "{\"host\":\"localhost\",\"port\":8080,\"max-connections\":16}", "compact");
return 0;
}
from_json
from_json<T>(value: JsonValue, unknown_keys: UnknownKeys = UnknownKeys.Ignore): Result<T, JsonError>
Converts a JsonValue to a T, the reverse of to_json.
A struct is made with its constructor that takes no arguments, then each
property found in the object is read in. A missing key leaves its
property at its default, unless the property is marked [Required],
which makes it an error; a property marked [Ignore] is never read. A
key that names no property is skipped, or with
unknown_keys = UnknownKeys.Reject is an error.
A number converts to an integer type when it has no fraction and fits
the type, and to float or double always. null converts only to an
Optional (as none) and to JsonValue.
An error names where in the document it is, as a path from the root $
in its value_path and at the start of its message, and has no line or
column, since there is no text: $.server.port: expected a number, found a string.
Type parameters
T— the type to convert to
Parameters
value— the JSON valueunknown_keys— what to do with an object key that names no property
Returns
- the converted value, or the first error
Example
import json;
struct Point {
var x: int;
var y: int;
}
main(): int {
var parsed = json.parse("{\"x\": 1, \"y\": 2}");
var p = json.from_json<Point>(parsed.value());
assert(p.value().y == 2, "a property");
return 0;
}
decode
decode<T>( text: String, unknown_keys: UnknownKeys = UnknownKeys.Ignore, comments: bool = false, path: String = "" ): Result<T, JsonError>
Parses JSON text and converts it to a T: parse, then from_json.
Type parameters
T— the type to convert to
Parameters
text— the JSON textunknown_keys— what to do with an object key that names no propertycomments— whether to accept JSONC: comments and trailing commaspath— the name of the text (a file name, say), which the error’sto_string()puts first
Returns
- the value, or the first error: a syntax error, or a conversion error with its path in the document; either has its line and column
Example
import json;
struct ServerConfig {
var host: String;
[Required]
var port: int;
var tags: Array<String>;
var timeout: int?;
}
main(): int {
var config = json.decode<ServerConfig>("{\"host\": \"a\", \"port\": 80, \"tags\": [\"x\"]}");
assert(config.value().port == 80, "a number");
assert(config.value().tags[0] == "x", "an array");
assert(config.value().timeout.is_empty(), "a missing optional");
var bad = json.decode<ServerConfig>("{\"port\": \"80\"}", path = "server.json");
assert(bad.error().to_string() == "server.json:1:10: error: $.port: expected a number, found a string", "a path");
return 0;
}
The serialize module
Serialization attributes: how a struct’s properties are written to and read from text formats.
json reads these attributes when it converts a struct (json.encode,
json.decode), and other formats will share them, so one set of
attributes describes a type for every format. Write them above a
property (or an enum or one of its cases: Tag, Untagged, Name);
import serialize brings them in, and import json alone is
enough too, since json exports them.
Example
import json;
struct ServerConfig {
var host: String;
[Name("max-connections")]
var max_connections: int;
[Required]
var port: int;
[Ignore]
var started_at: long;
}
Source: runtime/serialize/module.pep
Name
[Attribute] struct Name
Writes a property, or an enum case, under value rather than under its
own name.
Example
import json;
struct Limits {
[Name("max-connections")]
var max_connections: int;
}
Fields
value
const value: String
The key the property is written under, or the name the case is written as.
Ignore
[Attribute] struct Ignore
Leaves a property out: it is not written, and reading leaves it as it was.
Required
[Attribute] struct Required
Makes a missing key an error when reading. Without it, a missing key leaves the property at its default.
Tag
[Attribute] struct Tag
Writes an enum’s cases as objects that name their case under key,
rather than as {"Case": {...}}.
Without content, a case’s values sit beside the tag:
{"kind": "Circle", "radius": 2}. With content, they go in an object
under that key: {"kind": "Circle", "data": {"radius": 2}}. A case that
carries no values is {"kind": "Point"} either way. Write it above an
enum.
Example
import json;
[Tag("kind")]
enum Shape {
Circle(radius: int),
Point,
}
Fields
key
const key: String
The key that holds the case’s name.
content
const content: String = ""
The key that holds the case’s values, or empty to put them beside the tag.
Untagged
[Attribute] struct Untagged
Writes an enum’s cases with no tag at all: a case that carries values as
the object of its values ({"radius": 2}), and one that carries none as
its name ("Point").
Reading tries each case in order and takes the first that reads without an error, so list a case whose values are a superset of another’s first. Write it above an enum.
Example
import json;
[Untagged]
enum Size {
Fixed(px: int),
Auto,
}
The toml module
TOML: parse a document into a TomlValue, look values up and change
them, write tables back as documents, edit a document in place keeping
its comments and layout, and convert structs to TOML and back.
Bring it in with import toml. Parsing follows TOML 1.0 and checks all
of its rules; errors carry a line and column, like a compiler’s. Tables
keep their keys in the order they were written.
- TomlValue:
TomlValue,TomlDateTime - Parsing:
parse,TomlError - Writing:
to_string,to_document - Documents with comments:
parse_document,TomlDocument, for changing a file people edit, such aspep.toml, without reformatting it - Struct conversion:
encode,decode,to_toml,from_toml,UnknownKeys, with the attributes of the serialize module
Example
import toml;
main(): int {
var parsed = toml.parse("[package]\nname = \"demo\"\nversion = \"0.1.0\"\n");
if (parsed.is_err()) {
print(parsed.error().to_string());
return 1;
}
var root = parsed.value();
assert(root.lookup("package.name").value().as_string().value() == "demo", "a string");
var package = root["package"];
package["edition"] = toml.TomlValue.Integer(2026);
print(root.to_document());
return 0;
}
Source: runtime/toml/module.pep
TomlValue
TomlValue: a TOML value of any kind, and TomlDateTime, TOML’s dates
and times.
Source: runtime/toml/value.pep
TomlDateTime
struct TomlDateTime
A TOML date, time, or date and time: an offset date-time
(1979-05-27T07:32:00Z), a local date-time (1979-05-27T07:32:00), a
local date (1979-05-27) or a local time (07:32:00.5).
has_date, has_time and has_offset say which of the four it is;
the fields of a part it does not have are 0. to_string() writes it in
the RFC 3339 form TOML uses, with as many fraction digits as it was
read with.
Example
import toml;
var when = toml.TomlDateTime.offset(1979, 5, 27, 7, 32, 0, -420);
assert(when.to_string() == "1979-05-27T07:32:00-07:00", "an offset");
assert(toml.TomlDateTime.date(2024, 2, 29).to_string() == "2024-02-29", "a date");
Fields
year
var year: int
The year, 0 to 9999; 0 without a date.
month
var month: int
The month, 1 to 12; 0 without a date.
day
var day: int
The day of the month, from 1; 0 without a date.
hour
var hour: int
The hour, 0 to 23.
minute
var minute: int
The minute, 0 to 59.
second
var second: int
The second, 0 to 59.
nanosecond
var nanosecond: int
The fraction of the second, in nanoseconds.
fraction_digits
var fraction_digits: int
How many digits of the fraction to_string() writes, 0 to 9; 0
writes none.
offset_minutes
var offset_minutes: int
The offset from UTC in minutes (-07:00 is -420); 0 for Z.
has_date
var has_date: bool
Whether it has a date.
has_time
var has_time: bool
Whether it has a time of day.
has_offset
var has_offset: bool
Whether it has an offset from UTC, which only a date and time has.
Constructor
TomlDateTime()
construct()
Creates a local date-time at midnight on 0000-00-00, to fill in.
Static methods
date
static date(year: int, month: int, day: int): TomlDateTime
Returns a local date, such as 1979-05-27.
Parameters
year— the yearmonth— the month, from 1day— the day of the month, from 1
Returns
- the date
time
static time(hour: int, minute: int, second: int, nanosecond: int = 0): TomlDateTime
Returns a local time of day, such as 07:32:00.
Parameters
hour— the hourminute— the minutesecond— the secondnanosecond— the fraction of the second, in nanoseconds; written with as few digits as it needs
Returns
- the time
local
static local( year: int, month: int, day: int, hour: int, minute: int, second: int, nanosecond: int = 0 ): TomlDateTime
Returns a local date-time, with no offset: 1979-05-27T07:32:00.
Parameters
year— the yearmonth— the month, from 1day— the day of the month, from 1hour— the hourminute— the minutesecond— the secondnanosecond— the fraction of the second, in nanoseconds
Returns
- the date-time
offset
static offset( year: int, month: int, day: int, hour: int, minute: int, second: int, offset_minutes: int, nanosecond: int = 0 ): TomlDateTime
Returns a date-time at an offset from UTC: 1979-05-27T07:32:00Z.
Parameters
year— the yearmonth— the month, from 1day— the day of the month, from 1hour— the hourminute— the minutesecond— the secondoffset_minutes— the offset from UTC in minutes; 0 is writtenZnanosecond— the fraction of the second, in nanoseconds
Returns
- the date-time
Methods
to_string
to_string(): String
Returns the value as TOML writes it: 1979-05-27T07:32:00.5-07:00,
1979-05-27, 07:32:00.
Returns
- the text
TomlValue
enum TomlValue
A TOML value: a string, an integer, a float, a boolean, a date-time, an
array or a table. toml.parse makes a table from a document, and
to_document writes one back.
Look members up with value["key"] and elements with value[index],
which panic when there is none; try_get and lookup give none
instead, and lookup follows a dotted key ("package.name"). Then read
the value with an as_ method, which gives none when the value is of
another kind, or match on its case.
A Table keeps its keys in the order they were written. An Array or
Table case holds its array or map by reference, so changing them
through value[key] = ... or append changes every holder of the
value.
Example
import toml;
var package = toml.TomlValue.table();
package["name"] = toml.TomlValue.String("demo");
package["authors"] = toml.TomlValue.array([toml.TomlValue.String("Ada")]);
assert(package.to_string() == "{ name = \"demo\", authors = [\"Ada\"] }", "inline");
assert(package["name"].as_string().value() == "demo", "a lookup");
Cases
String
String(value: String)
A string.
Parameters
value— the text
Integer
Integer(value: long)
A 64-bit integer.
Parameters
value— the integer
Float
Float(value: double)
A 64-bit float, including inf and nan.
Parameters
value— the number
Boolean
Boolean(value: bool)
true or false.
Parameters
value— the boolean
DateTime
DateTime(value: TomlDateTime)
A date, a time, or both.
Parameters
value— the date-time
Array
Array(items: Array<TomlValue>)
An array, holding its elements in order.
Parameters
items— the elements
Table
Table(members: OrderedMap<String, TomlValue>)
A table, holding its members in the order they were written.
Parameters
members— the members, by key
Static methods
table
static table(): TomlValue
Returns a new, empty table.
Returns
- a
Tablewith no members
array
static array(items: Array<TomlValue> = Array<TomlValue>()): TomlValue
Returns an array holding items, which it keeps by reference rather
than copying.
Parameters
items— the elements; an empty array when not given
Returns
- an
Arrayofitems
Methods
operator []
[](key: String): TomlValue
Reads value[key]: the member named key of a table.
Parameters
key— the member’s key
Returns
- the member
Panics
- if this is not a table or has no member named
key(“no key ‘k’ in the TOML table”); usetry_getto check
operator []
[](key: String, member: TomlValue): TomlValue
Writes value[key] = member: sets the member named key of a
table. A new key goes at the end; an existing key keeps its place.
Parameters
key— the member’s keymember— the value to store
Returns
member, the value of the assignment expression
Panics
- if this is not a table (“TomlValue[key] = value needs a table”)
operator []
[](index: ulong): TomlValue
Reads value[index]: an element of an array.
Parameters
index— the element’s position, from 0
Returns
- the element
Panics
- if this is not an array (“TomlValue[index] needs an array”)
- if
indexis not less than the array’s count (asArray’s indexer does)
operator []
[](index: ulong, element: TomlValue): TomlValue
Writes value[index] = element: replaces an element of an array.
Parameters
index— the element’s position, from 0element— the value to store
Returns
element, the value of the assignment expression
Panics
- if this is not an array (“TomlValue[index] = value needs an array”)
- if
indexis not less than the array’s count
try_get
try_get(key: String): TomlValue?
Returns the member named key, when this is a table that has one.
Parameters
key— the member’s key
Returns
- the member, or none
lookup
lookup(key: String): TomlValue?
Returns the value at a dotted key below this table, read as TOML
reads a key: package.name, dependencies."my.crate".version.
Parameters
key— the dotted key
Returns
- the value, or none when a part is missing, is not a table, or
keyis not a valid TOML key
Example
import toml;
var root = toml.parse("[package]\nname = \"demo\"").value();
assert(root.lookup("package.name").value().as_string().value() == "demo", "found");
assert(root.lookup("package.version").is_empty(), "missing");
get_path
get_path(keys: Array<String>): TomlValue?
Returns the value at the path of keys keys below this table.
Parameters
keys— the keys, outermost first; empty gives this value
Returns
- the value, or none when a part is missing or is not a table
append
append(element: TomlValue)
Appends element to the end of an array.
Parameters
element— the value to append
Panics
- if this is not an array (“TomlValue.append needs an array”)
contains
contains(key: String): bool
Returns whether this is a table with a member named key.
Parameters
key— the key to look for
Returns
- true when the member exists
remove
remove(key: String): bool
Removes the member named key from a table.
Parameters
key— the key to remove
Returns
- true when this is a table and the member was there
type_name
type_name(): String
Returns what kind of value this is, as errors name it: string,
integer, float, boolean, date-time, array or table.
Returns
- the kind’s name
as_string
as_string(): String?
Returns the string, when this is one.
Returns
- the text of a
String, or none
as_int
as_int(): long?
Returns the integer, when this is one.
Returns
- the value of an
Integer, or none
as_float
as_float(): double?
Returns the number as a double, when this is a Float or an
Integer.
Returns
- the number, or none
as_bool
as_bool(): bool?
Returns the boolean, when this is one.
Returns
- the value of a
Boolean, or none
as_date_time
as_date_time(): TomlDateTime?
Returns the date-time, when this is one.
Returns
- the value of a
DateTime, or none
as_array
as_array(): Array<TomlValue>?
Returns the elements, when this is an array. The array is the one this value holds, not a copy.
Returns
- the elements of an
Array, or none
as_table
as_table(): OrderedMap<String, TomlValue>?
Returns the members, when this is a table. The map is the one this value holds, not a copy.
Returns
- the members of a
Table, or none
to_string
to_string(): String
Returns the value as TOML writes it after a key’s =, on one line:
"text", 42, 1.5, [1, 2], { name = "demo" }.
Returns
- the TOML text
to_document
to_document(): String
Returns a table as a TOML document: its plain members as key = value lines, then each table member under a [header] and each
array of tables as [[header]] sections, nested tables after their
parent’s members.
Returns
- the document text, ending with a line break unless it is empty
Panics
- if this is not a table (“TomlValue.to_document needs a table”)
Example
import toml;
var root = toml.TomlValue.table();
root["title"] = toml.TomlValue.String("demo");
var server = toml.TomlValue.table();
server["port"] = toml.TomlValue.Integer(8080);
root["server"] = server;
assert(root.to_document() == "title = \"demo\"\n\n[server]\nport = 8080\n", "a document");
Parsing
parse: reading a TOML document into a TomlValue, and TomlError,
what it reports when the text is not TOML.
Source: runtime/toml/parse.pep
TomlError
struct TomlError
What parse reports when the text is not TOML, and what decode
reports when a value does not fit its property: what is wrong, and the
line and column where it is.
to_string() renders it like a compiler error:
pep.toml:4:12: error: expected '='.
Fields
message
var message: String
What is wrong, worded like a compiler error (expected '=').
path
var path: String
The path given to parse, which names the text in to_string();
empty when none was given.
line
var line: ulong
The line of the error, from 1; 0 for an error with no text to point
into, such as one from from_toml.
column
var column: ulong
The column of the error, from 1, counting bytes as clang does; 0
when line is.
offset
var offset: ulong
The byte offset of the error in the text, from 0.
value_path
var value_path: String
Where in the document the value an error is about is, as a path from
the root $ ($.package.version, $.bin[0].name); empty for an
error in the text’s syntax. decode and from_toml set it, and
start message with it.
Constructor
TomlError()
construct(message: String = "", path: String = "", line: ulong = 1, column: ulong = 1, offset: ulong = 0)
Creates an error.
Parameters
message— what is wrongpath— the name of the text, or emptyline— the line of the error, from 1column— the column of the error, from 1offset— the byte offset of the error, from 0
Methods
to_string
to_string(): String
Returns the error as a compiler prints one:
path:line:column: error: message, without the path: when there
is no path and without line:column: when line is 0.
Returns
- the rendered error
parse
parse(text: String, path: String = "", max_depth: ulong = 1000): Result<TomlValue, TomlError>
Parses a TOML 1.0 document into a TomlValue, a Table.
Every rule of TOML 1.0 is checked: a key or table defined twice, a table
extended after it was closed (an inline table, or one written with
dotted keys in another section), strings, numbers and dates as the
specification spells them. The text must be valid UTF-8; a byte-order
mark at the start is skipped. Tables keep their keys in the order they
were written. A line break in a multi-line string reads as \n, also
when the text has \r\n.
Arrays and inline tables nest as deeply as the text says, so the parser
keeps its own stack rather than recursing, and max_depth bounds it.
Parameters
text— the TOML textpath— the name of the text (a file name, say), which the error’sto_string()puts firstmax_depth— how deeply arrays and inline tables may nest
Returns
- the document’s table, or an error naming the first problem and its line and column
Example
import toml;
var parsed = toml.parse("[server]\nport = 8080\n");
assert(parsed.value()["server"]["port"].as_int().value() == 8080, "a lookup");
var bad = toml.parse("name \"demo\"", path = "pep.toml");
assert(bad.error().to_string() == "pep.toml:1:6: error: expected '=' after the key", "where");
Writing
Writing TOML: values on one line, and tables as documents.
Source: runtime/toml/write.pep
Documents with comments
TomlDocument: a TOML document changed in place, keeping its comments,
blank lines and layout, as pepper package add changes pep.toml.
Source: runtime/toml/document.pep
TomlDocument
struct TomlDocument
A TOML document that keeps the text it was read from, so a file people edit can be changed and written back without reformatting it.
set and remove change only the text they must: a changed value is
rewritten where it stands, a new key goes after the last key of its
table, a new table goes at the end, and a removed key or table takes
its lines with it, along with the comment lines right above them. Every
other byte, comments included, stays as it was. Each edit parses the
result again, so root() and get always see the current text.
Keys are given as arrays, outermost first (["dependencies", "json5"]),
so a key holding a dot needs no quoting. An edit cannot reach inside an
array of tables ([[bin]]); set or remove the whole array instead.
Example
import toml;
var text = "[package]\nname = \"demo\" # the name\n\n[dependencies]\njson5 = \"1.0\"\n";
var document = toml.parse_document(text).value();
document.set(["package", "version"], toml.TomlValue.String("0.2.0")).value();
document.set(["dependencies", "yaml"], toml.TomlValue.String("0.3")).value();
document.remove(["dependencies", "json5"]).value();
assert(
document.to_string()
== "[package]\nname = \"demo\" # the name\nversion = \"0.2.0\"\n\n[dependencies]\nyaml = \"0.3\"\n",
"only the edits change"
);
Constructor
TomlDocument()
construct()
Creates an empty document.
Methods
root
root(): TomlValue
Returns the document’s table, as parse reads the current text.
Changing it does not change the document; use set and remove.
Returns
- the root table
get
get(keys: Array<String>): TomlValue?
Returns the value at keys.
Parameters
keys— the keys, outermost first
Returns
- the value, or none when a part is missing or is not a table
lookup
lookup(key: String): TomlValue?
Returns the value at a dotted key, read as TOML reads a key:
package.name, dependencies."my.package".
Parameters
key— the dotted key
Returns
- the value, or none when it is missing or
keyis not a valid TOML key
set
set(keys: Array<String>, value: TomlValue, inline: bool = false): Result<void, TomlError>
Sets the value at keys, adding the key, and any tables above it,
when it is missing.
A key that is there has its value rewritten in place, on one line;
a key inside an inline table rewrites that inline table. A new key
goes after the last key of the table it belongs to, indented like
it, or under a new [header] at the end when that table has no
section to put it in. A new table value (or array of tables) is
written as its own [header] section at the end, unless inline is
true, which writes it { ... } on the key’s line instead, as
cargo add writes a dependency.
Parameters
keys— the keys, outermost firstvalue— the value to storeinline— whether a new table value is written as an inline table rather than a section
Returns
- nothing, or an error when a part of
keysholds a value that is not a table, or lies inside an array of tables
Panics
- if
keysis empty (“TomlDocument.set needs a key”)
remove
remove(keys: Array<String>): Result<bool, TomlError>
Removes the key or table at keys, with its lines and the comment
lines right above them. Removing a table removes its sections, the
tables below it, and any dotted keys that define it.
Parameters
keys— the keys, outermost first
Returns
- whether something was removed, or an error when a part of
keyslies inside an array of tables
Panics
- if
keysis empty (“TomlDocument.remove needs a key”)
to_string
to_string(): String
Returns the document’s text, with every edit made.
Returns
- the TOML text
parse_document
parse_document(text: String, path: String = ""): Result<TomlDocument, TomlError>
Parses a TOML document into a TomlDocument, to change it in place.
The text is read as parse reads it. Use it to change a file people
edit, such as pep.toml, and write it back with its comments and
layout; use parse when only the values matter.
Parameters
text— the TOML textpath— the name of the text (a file name, say), which an error’sto_string()puts first
Returns
- the document, or an error naming the first problem and its line and column
Struct conversion
Struct conversion: encode and decode turn Pepper values into TOML
documents and back, through reflection, with no per-type code to write.
They read the same serialize attributes json does, so one set of
attributes describes a type for both formats.
Source: runtime/toml/convert.pep
UnknownKeys
enum UnknownKeys
What decode does with a table key that names no property of the
struct it is read into.
Cases
Ignore
Ignore
Skips the key and its value.
Reject
Reject
Fails with an error naming the key and its path.
to_toml
to_toml<T>(value: T): TomlValue?
Converts value to a TomlValue.
T may be a number type, bool, String, TomlDateTime, an enum (a
case that carries no values as its name, "Point"; one that carries
values as { Circle = { radius = 2 } }), Optional<U>, Array<U>, a
map with String keys (HashMap, OrderedMap or Map) as a table,
TomlValue itself, or a struct, whose public properties become a
table’s members in declaration order. Element and property types follow
the same rules, so structs nest.
TOML has no null, so what JSON writes as null is left out: an empty
Optional, a String or struct property that holds no object, and such
an element of an array or map. A ulong past the largest TOML integer,
2^63 - 1, is written as a float.
A struct property takes its key from [Name("key")] when it has one,
and is left out when marked [Ignore] (the attributes of the
serialize module, which toml shares with json).
The conversion is generated while compiling, by a static block per
type. A type it cannot convert, such as a Result, is a compile error
naming the property:
'encode' cannot convert property 'outcome' of type 'Result<int>' to TOML.
Type parameters
T— the type of the value
Parameters
value— the value to convert
Returns
- the value as TOML, or none when there is nothing to write
Example
import toml;
struct Point {
var x: int;
var y: int;
}
main(): int {
var p = Point();
p.x = 1;
p.y = 2;
assert(toml.to_toml(p).value()["y"].as_int().value() == 2, "a member");
return 0;
}
encode
encode<T>(value: T): String
Writes value, a struct or a map, as a TOML document, converting it as
to_toml does.
Type parameters
T— the type of the value
Parameters
value— the value to write
Returns
- the document text
Panics
- if
valuedoes not convert to a table, as a number or an array does (“toml.encode needs a value that converts to a table”)
Example
import toml;
struct Package {
var name: String;
var version: String;
var authors: Array<String>;
}
struct Manifest {
var package: Package;
[Name("build-script")]
var build_script: String?;
}
main(): int {
var manifest = Manifest();
manifest.package = Package();
manifest.package.name = "demo";
manifest.package.version = "0.1.0";
manifest.package.authors = ["Ada"];
var text = toml.encode(manifest);
assert(text == "[package]\nname = \"demo\"\nversion = \"0.1.0\"\nauthors = [\"Ada\"]\n", "a document");
return 0;
}
from_toml
from_toml<T>(value: TomlValue, unknown_keys: UnknownKeys = UnknownKeys.Ignore): Result<T, TomlError>
Converts a TomlValue to a T, the reverse of to_toml.
A struct is made with its constructor that takes no arguments, then each
property found in the table is read in. A missing key leaves its
property at its default (an empty Optional, for one), unless the
property is marked [Required], which makes it an error; a property
marked [Ignore] is never read. A key that names no property is
skipped, or with unknown_keys = UnknownKeys.Reject is an error.
An integer converts to an integer type it fits, and to float or
double; a float converts only to float or double.
An error names where in the document it is, as a path from the root $
in its value_path and at the start of its message, and has no line or
column, since there is no text: $.package.version: expected a string, found an integer.
Type parameters
T— the type to convert to
Parameters
value— the TOML valueunknown_keys— what to do with a key that names no property
Returns
- the converted value, or the first error
decode
decode<T>( text: String, unknown_keys: UnknownKeys = UnknownKeys.Ignore, path: String = "" ): Result<T, TomlError>
Parses a TOML document and converts it to a T: parse, then
from_toml.
Type parameters
T— the type to convert to
Parameters
text— the TOML textunknown_keys— what to do with a key that names no propertypath— the name of the text (a file name, say), which the error’sto_string()puts first
Returns
- the value, or the first error: a syntax error, or a conversion error with its path in the document; either has its line and column when the value it is about is written in the text
Example
import toml;
struct Package {
[Required]
var name: String;
var version: String;
var authors: Array<String>;
var edition: int?;
}
struct Manifest {
[Required]
var package: Package;
}
main(): int {
var text = "[package]\nname = \"demo\"\nversion = \"0.1.0\"\nauthors = [\"Ada\"]\n";
var manifest = toml.decode<Manifest>(text);
assert(manifest.value().package.name == "demo", "a string");
assert(manifest.value().package.authors[0] == "Ada", "an array");
assert(manifest.value().package.edition.is_empty(), "a missing optional");
var bad = toml.decode<Manifest>("[package]\nname = 7\n", path = "pep.toml");
assert(
bad.error().to_string() == "pep.toml:2:8: error: $.package.name: expected a string, found an integer",
"where"
);
return 0;
}
The compress module
Compression: DEFLATE (RFC 1951) with its zlib and gzip framings, in one call or as streams, and the CRC-32 and Adler-32 checksums.
Bring it in with import compress. It is written in Pepper, so it needs
no zlib, unless the program is built with -D USE_ZLIB, which makes it
use zlib with the same API. Corrupt data is an io.IoError whose
is_invalid_input() holds.
- Formats and streams:
Format,Deflate,Zlib,Gzip,DecompressReader,CompressWriter - Checksums:
Crc32,Adler32
Example
import compress;
main(): int {
var data = "to be or not to be, that is the question".bytes();
var packed = compress.Gzip.compress(data);
assert(compress.Gzip.decompress(packed).value() == data, "round trip");
return 0;
}
Source: runtime/compress/module.pep
Formats and streams
Format, the one-call Deflate, Zlib and Gzip functions, and the
streaming DecompressReader and CompressWriter.
Source: runtime/compress/format.pep
Format
enum Format
The framing around DEFLATE-compressed data.
Example
import compress;
var packed = compress.Gzip.compress("hi".bytes());
var reader = compress.DecompressReader(io.ByteReader(packed), compress.Format.Gzip);
Cases
Raw
Raw
Raw DEFLATE (RFC 1951) with no header or checksum, as zip archives
and HTTP’s Content-Encoding: deflate from some servers carry it.
Zlib
Zlib
zlib (RFC 1950): a two-byte header, DEFLATE, then an Adler-32
checksum. PNG images and HTTP’s deflate encoding use it.
Gzip
Gzip
gzip (RFC 1952): a header, DEFLATE, then a CRC-32 and the length.
.gz files and HTTP’s gzip encoding use it. Reading accepts
several members one after another, as cat a.gz b.gz makes.
Deflate
struct Deflate
Raw DEFLATE (RFC 1951) data, compressed or decompressed in one call.
Example
import compress;
var data = "abcabcabcabc".bytes();
var packed = compress.Deflate.compress(data);
assert(compress.Deflate.decompress(packed).value() == data, "round trip");
Static methods
compress
static compress(data: u8[], level: int = 6): u8[]
Compresses data into raw DEFLATE data.
Parameters
data— the bytes to compresslevel— from 0 (store without compressing) through 9 (smallest output, slowest); 6 balances the two, as zlib’s default does
Returns
- the compressed bytes
Panics
- if
levelis not 0 through 9 (“compression level must be 0 through 9”)
decompress
static decompress(data: u8[]): io.IoResult<u8[]>
Decompresses raw DEFLATE data. Bytes after the end of the compressed data are ignored.
Parameters
data— the compressed bytes
Returns
- the decompressed bytes; an
is_invalid_input()error whendatais not valid DEFLATE data or ends too soon
Zlib
struct Zlib
zlib (RFC 1950) data, compressed or decompressed in one call.
Example
import compress;
var packed = compress.Zlib.compress("hello".bytes());
assert(packed[0] == 0x78, "the zlib header");
assert(compress.Zlib.decompress(packed).value() == "hello".bytes(), "round trip");
Static methods
compress
static compress(data: u8[], level: int = 6): u8[]
Compresses data into a zlib stream.
Parameters
data— the bytes to compresslevel— from 0 (store without compressing) through 9 (smallest output, slowest); 6 balances the two
Returns
- the compressed bytes
Panics
- if
levelis not 0 through 9 (“compression level must be 0 through 9”)
compress
static compress(data: u8[], dictionary: u8[], level: int = 6): u8[]
Compresses data into a zlib stream that starts from a preset
dictionary: bytes the data is likely to repeat, such as the common
keys of small JSON messages, which the stream can then refer back to
without carrying them. The header names the dictionary by its
Adler-32, and decompressing needs the same dictionary. Only its last
32 KiB are used.
Parameters
data— the bytes to compressdictionary— the preset dictionarylevel— from 0 (store without compressing) through 9 (smallest output, slowest); 6 balances the two
Returns
- the compressed bytes
Panics
- if
levelis not 0 through 9 (“compression level must be 0 through 9”)
Example
import compress;
var dictionary = "{\"name\": \"\", \"email\": \"\"}".bytes();
var data = "{\"name\": \"Ada\", \"email\": \"ada@example.com\"}".bytes();
var packed = compress.Zlib.compress(data, dictionary);
assert(compress.Zlib.decompress(packed, dictionary).value() == data, "round trip");
decompress
static decompress(data: u8[]): io.IoResult<u8[]>
Decompresses a zlib stream and checks its Adler-32 checksum. Bytes after the stream are ignored. A stream made with a preset dictionary needs the overload that takes one.
Parameters
data— the compressed bytes
Returns
- the decompressed bytes; an
is_invalid_input()error whendatais not a valid zlib stream, ends too soon, fails its checksum, or needs a preset dictionary
decompress
static decompress(data: u8[], dictionary: u8[]): io.IoResult<u8[]>
Decompresses a zlib stream that may need a preset dictionary, and checks its Adler-32 checksum. A stream that does not ask for a dictionary decompresses as without one.
Parameters
data— the compressed bytesdictionary— the preset dictionary the stream was compressed with
Returns
- the decompressed bytes; an
is_invalid_input()error whendatais not a valid zlib stream, ends too soon, fails its checksum, or names a dictionary whose Adler-32 is not that ofdictionary
Gzip
struct Gzip
gzip (RFC 1952) data, as in .gz files, compressed or decompressed in
one call.
Example
import compress;
import io;
var packed = compress.Gzip.compress("hello".bytes());
io.File.write_bytes(io.Path("hello.txt.gz"), packed);
assert(compress.Gzip.decompress(packed).value() == "hello".bytes(), "round trip");
Static methods
compress
static compress(data: u8[], level: int = 6): u8[]
Compresses data into one gzip member with no file name and no
modification time.
Parameters
data— the bytes to compresslevel— from 0 (store without compressing) through 9 (smallest output, slowest); 6 balances the two
Returns
- the compressed bytes
Panics
- if
levelis not 0 through 9 (“compression level must be 0 through 9”)
decompress
static decompress(data: u8[]): io.IoResult<u8[]>
Decompresses gzip data, checking each member’s CRC-32 and length. Several members one after another decompress to their contents joined. The header’s file name, comment and extra field are skipped.
Parameters
data— the compressed bytes
Returns
- the decompressed bytes; an
is_invalid_input()error whendatais not valid gzip data, ends too soon, fails a check, or has bytes after the last member
DecompressReader
struct DecompressReader
A Reader that decompresses another stream as it is read, such as a
.gz file or an HTTP response body, without holding all of it in memory.
It reads its source in chunks of up to 64 KiB and keeps at most about
160 KiB of output, however large the data. Corrupt or truncated data is an
is_invalid_input() error from read, after the bytes decoded before
the problem; an error from the source is passed on.
Example
import compress;
import io;
var file = io.File.open(io.Path("notes.txt.gz")).value();
var reader = compress.DecompressReader(file, compress.Format.Gzip);
for (var line in reader.lines()) {
print(line.value());
}
Constructors
DecompressReader()
construct(source: io.Reader, format: Format)
Makes a reader of the decompressed contents of source.
Parameters
source— the stream of compressed bytesformat— the framing of the compressed data
DecompressReader()
construct(source: io.Reader, format: Format, dictionary: u8[])
Makes a reader of the decompressed contents of source, which was
compressed with a preset dictionary (see Zlib.compress). A zlib
stream uses it when its header asks for it; raw DEFLATE data always
starts from it.
Parameters
source— the stream of compressed bytesformat—Format.ZliborFormat.Rawdictionary— the preset dictionary
Panics
- if
formatisFormat.Gzip(“gzip data has no preset dictionary”)
Methods
read
read(buffer: u8[], max: ulong): io.IoResult<ulong>
Decompresses up to max more bytes and appends them to buffer.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to read
Returns
- the number of bytes appended, 0 at the end of the compressed data; an error when the data is invalid or the source fails
read_all
read_all(): io.IoResult<u8[]>
Decompresses the rest of the stream.
Returns
- the decompressed bytes not read yet; an error as for
read
CompressWriter
struct CompressWriter
A Writer that compresses what is written to it and writes the result
to another stream, such as a .gz file.
Compressed bytes reach the sink in pieces as blocks fill; finish()
writes the rest and the format’s trailer, and must be called once at the
end, since dropping the writer does not. It does not close the sink.
Example
import compress;
import io;
var file = io.File.create(io.Path("log.txt.gz")).value();
var writer = compress.CompressWriter(file, compress.Format.Gzip);
writer.print_line("first line");
writer.print_line("second line");
writer.finish();
file.close();
Constructors
CompressWriter()
construct(sink: io.Writer, format: Format, level: int = 6)
Makes a writer that compresses into sink.
Parameters
sink— the stream the compressed bytes are written toformat— the framing to writelevel— from 0 (store without compressing) through 9 (smallest output, slowest); 6 balances the two
Panics
- if
levelis not 0 through 9 (“compression level must be 0 through 9”)
CompressWriter()
construct(sink: io.Writer, format: Format, dictionary: u8[], level: int = 6)
Makes a writer that compresses into sink, starting from a preset
dictionary (see Zlib.compress). A zlib header names the dictionary
by its Adler-32; raw DEFLATE data carries no sign of it.
Parameters
sink— the stream the compressed bytes are written toformat—Format.ZliborFormat.Rawdictionary— the preset dictionary; only its last 32 KiB are usedlevel— from 0 (store without compressing) through 9 (smallest output, slowest); 6 balances the two
Panics
- if
levelis not 0 through 9 (“compression level must be 0 through 9”), or ifformatisFormat.Gzip(“gzip data has no preset dictionary”)
Methods
write
write(data: u8[]): io.IoResult<ulong>
Compresses all of data.
Parameters
data— the bytes to compress
Returns
data.count; an error when writing to the sink fails
Panics
- if
finish()was called (“CompressWriter used after finish()”)
write_all
write_all(data: u8[]): io.IoResult<void>
Compresses all of data.
Parameters
data— the bytes to compress
Returns
- an Ok result; an error when writing to the sink fails
Panics
- if
finish()was called (“CompressWriter used after finish()”)
finish
finish(): io.IoResult<void>
Compresses what is still buffered, writes the format’s trailer, and ends the stream. Later calls do nothing.
Returns
- an Ok result; an error when writing to the sink fails
Checksums
Crc32 and Adler32: the checksums gzip and zlib streams carry, for
checking data as it is read or written.
Source: runtime/compress/checksum.pep
Crc32
struct Crc32
The CRC-32 checksum (IEEE 802.3, the polynomial gzip, zip and PNG use), computed over data given in one or more pieces.
Example
import compress;
var crc = compress.Crc32();
crc.update("hello ".bytes());
crc.update("world".bytes());
assert(crc.value == compress.Crc32.of("hello world".bytes()), "same in pieces");
assert(compress.Crc32.of("123456789".bytes()) == 0xCBF43926, "the check value");
Properties
value
var value: uint { get; }
The checksum of everything given to update so far.
Constructor
Crc32()
construct()
Makes a checksum of no data yet (value is 0).
Static methods
of
static of(data: u8[]): uint
Returns the CRC-32 of data.
Parameters
data— the bytes to check
Returns
- the checksum
Methods
update
update(data: u8[])
Adds data to the checksum.
Parameters
data— the next bytes of the data
reset
reset()
Starts over, as if no data had been given.
Adler32
struct Adler32
The Adler-32 checksum (RFC 1950, the one zlib streams carry), computed over data given in one or more pieces.
Example
import compress;
assert(compress.Adler32.of("Wikipedia".bytes()) == 0x11E60398, "the example");
Properties
value
var value: uint { get; }
The checksum of everything given to update so far.
Constructor
Adler32()
construct()
Makes a checksum of no data yet (value is 1).
Static methods
of
static of(data: u8[]): uint
Returns the Adler-32 of data.
Parameters
data— the bytes to check
Returns
- the checksum
Methods
update
update(data: u8[])
Adds data to the checksum.
Parameters
data— the next bytes of the data
reset
reset()
Starts over, as if no data had been given.
The archive module
Archives: tar files, read entry by entry, extracted safely into a directory, or written.
Bring it in with import archive. Combine it with compress for
.tar.gz: wrap the file in a compress.DecompressReader to read one, or
in a compress.CompressWriter to write one. A damaged archive is an
io.IoError whose is_invalid_input() holds.
- Tar archives:
TarReader,TarWriter,TarEntry,EntryKind - Entry paths:
safe_components
Example
import archive;
import io;
main(): int {
var out = io.ByteWriter();
var writer = archive.TarWriter(out);
writer.append_file("hello.txt", "hi\n".bytes());
writer.finish();
var reader = archive.TarReader(io.ByteReader(out.bytes));
var entry = reader.next_entry().value().value();
assert(entry.path == "hello.txt" && entry.size == 3, "the header");
assert(reader.read_all().value() == "hi\n".bytes(), "the contents");
return 0;
}
Source: runtime/archive/module.pep
Tar archives
TarReader and TarWriter: tar archives (POSIX ustar, with the pax and
GNU extensions for long names), read entry by entry from a stream,
extracted safely into a directory, or written.
Source: runtime/archive/tar.pep
EntryKind
enum EntryKind
What a tar entry is.
Cases
File
File
A regular file, whose contents follow the entry’s header.
Directory
Directory
A directory.
Symlink
Symlink
A symbolic link to TarEntry.link_target.
HardLink
HardLink
A hard link to the earlier entry named TarEntry.link_target.
Other
Other
Anything else: a device, a FIFO, or a type this reader does not know.
TarEntry
struct TarEntry
One entry of a tar archive: its header, without its contents.
Example
import archive;
var entry = archive.TarEntry("bin/run.sh", archive.EntryKind.File);
entry.mode = 493; # octal 755: rwxr-xr-x
Fields
path
var path: String
The path in the archive, with / between components, as stored (a
directory’s may end with /).
kind
var kind: EntryKind
What the entry is.
size
var size: ulong
The length of the contents in bytes; 0 for anything but a file.
mode
var mode: uint
The permission bits, such as 420 (octal 644, rw-r--r--).
modified
var modified: long
The modification time, in seconds since 1970-01-01 UTC.
link_target
var link_target: String
The target of a symlink or hard link; empty otherwise.
Constructor
TarEntry()
construct(path: String = "", kind: EntryKind = EntryKind.File)
Makes an entry with the given path and kind, mode 420 (octal 644) for a file and 493 (octal 755) otherwise, size 0 and modification time 0.
Parameters
path— the path in the archivekind— what the entry is
TarReader
struct TarReader
Reads a tar archive from a stream, one entry at a time: next_entry()
moves to the next entry and returns its header, then read (it is an
io.Reader) reads that entry’s contents. Contents not read are skipped.
It reads POSIX ustar archives and the pax (x) and GNU (L, K)
extensions for long paths, link targets and sizes, which is what GNU tar,
bsdtar and Git (git archive, and the .tar.gz snapshots GitHub, GitLab
and Codeberg serve) write. Global pax headers (g) are skipped.
Wrap the source in a compress.DecompressReader to read a .tar.gz.
Example
import archive;
import compress;
import io;
var file = io.File.open(io.Path("release.tar.gz")).value();
var tar = archive.TarReader(compress.DecompressReader(file, compress.Format.Gzip));
while (true) {
var entry = tar.next_entry().value();
if (entry.is_empty()) {
break;
}
print(entry.value().path);
}
Constructor
TarReader()
construct(source: io.Reader)
Makes a reader of the archive in source, before its first entry.
Parameters
source— the stream of the archive
Methods
next_entry
next_entry(): io.IoResult<TarEntry?>
Moves to the next entry, skipping what is left of the current one’s contents, and returns its header.
The archive ends at two zero blocks, as tar writes, or at the end of the stream between entries.
Returns
- the next entry, or none at the end of the archive; an
is_invalid_input()error for a damaged or truncated archive, or the source’s error
read
read(buffer: u8[], max: ulong): io.IoResult<ulong>
Reads up to max more bytes of the current entry’s contents and
appends them to buffer.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to read
Returns
- the number of bytes appended, 0 at the end of the entry’s contents (or before the first
next_entry()); an error when the archive is truncated or the source fails
read_all
read_all(): io.IoResult<u8[]>
Reads the rest of the current entry’s contents.
Returns
- the bytes not read yet; an error as for
read
extract
extract(target: io.Path, strip_components: ulong = 0): io.IoResult<void>
Extracts the rest of the archive into the directory target,
creating it and any directories the entries need.
Each entry’s path is checked first, so that no archive can write
outside target: an absolute path, a .. component, a \ or a :
(which Windows would read as a separator or a drive) is an
is_invalid_input() error. Files are written (replacing a file
already there) and directories created; a hard link becomes a copy of
the file it names, which must be an earlier entry. Devices and FIFOs
are skipped.
Each file and directory gets the entry’s modification time and, on
Linux and macOS, its permission bits less the process umask, as
tar gives them by default, so a script keeps its executable bit;
the setuid, setgid and sticky bits are never applied. A directory’s
mode and time are set once everything inside it is written, so a
read-only directory can still be filled. A directory that already
exists keeps its own. On Windows and WASI permission bits are not
applied.
Symlinks are made after every other entry, so no file is ever
written through one, and only when they stay inside target: a link
whose target is absolute or climbs out of target with .., or that
would be made inside another symlink, is skipped. Windows, where
making a symlink needs Developer Mode or an administrator, skips them
all.
Parameters
target— the directory to extract intostrip_components— how many leading path components to drop from each entry, as tar’s--strip-componentsdoes: 1 drops thename-version/directory that GitHub’s snapshots put everything in. An entry with no components left is skipped.
Returns
- an Ok result once every entry is extracted; an
is_invalid_input()error for an unsafe path or a damaged archive, or the error of a file that could not be written
Example
import archive;
import compress;
import io;
var file = io.File.open(io.Path("pepper-main.tar.gz")).value();
var tar = archive.TarReader(compress.DecompressReader(file, compress.Format.Gzip));
tar.extract(io.Path(".pep/deps/pepper"), strip_components = 1).value();
TarWriter
struct TarWriter
Writes a tar archive to a stream: each append writes one entry’s
header and contents, and finish() writes the end-of-archive marker.
It writes POSIX ustar headers, and a pax header before an entry whose
path or link target does not fit one (a path over 100 bytes that cannot
be split at a / into 155 and 100), so every path is kept whole. Owner
ids are 0 and owner names empty. Wrap the sink in a
compress.CompressWriter to write a .tar.gz.
Example
import archive;
import io;
var out = io.ByteWriter();
var tar = archive.TarWriter(out);
tar.append_directory("docs");
tar.append_file("docs/readme.txt", "hello\n".bytes());
tar.finish();
assert(out.bytes.count == 4 * 512 + 2 * 512, "headers, contents, end marker");
Constructor
TarWriter()
construct(sink: io.Writer)
Makes a writer of a new archive into sink.
Parameters
sink— the stream the archive is written to
Methods
append_file
append_file(path: String, data: u8[], mode: uint = 420): io.IoResult<void>
Appends a regular file holding data.
Parameters
path— the file’s path in the archive, with/between componentsdata— the file’s contentsmode— the permission bits; 420 is octal 644 (rw-r--r--)
Returns
- an Ok result; an error when writing to the sink fails
Panics
- if
finish()was called (“TarWriter used after finish()”)
append_directory
append_directory(path: String, mode: uint = 493): io.IoResult<void>
Appends a directory.
Parameters
path— the directory’s path in the archive; a final/is added when missingmode— the permission bits; 493 is octal 755 (rwxr-xr-x)
Returns
- an Ok result; an error when writing to the sink fails
Panics
- if
finish()was called (“TarWriter used after finish()”)
append_symlink
append_symlink(path: String, target: String): io.IoResult<void>
Appends a symbolic link.
Parameters
path— the link’s path in the archivetarget— the path the link points to
Returns
- an Ok result; an error when writing to the sink fails
Panics
- if
finish()was called (“TarWriter used after finish()”)
append
append(entry: TarEntry, data: u8[]): io.IoResult<void>
Appends an entry with the given header and contents. The size
written is data.count for a file and 0 for anything else, whatever
entry.size holds.
Parameters
entry— the entry’s headerdata— the contents of a file entry; ignored for other kinds
Returns
- an Ok result; an error when writing to the sink fails
Panics
- if
finish()was called (“TarWriter used after finish()”)
append_stream
append_stream(entry: TarEntry, source: io.Reader): io.IoResult<void>
Appends a file entry whose contents are read from source: exactly
entry.size bytes, so a large file need not be in memory.
Parameters
entry— the entry’s header; itskindmust beFilesource— the stream holding the contents
Returns
- an Ok result; an error when reading or writing fails, or an
is_invalid_input()error whensourceends beforeentry.sizebytes (the archive is then incomplete)
Panics
- if
finish()was called (“TarWriter used after finish()”) - if
entry.kindis notFile(“append_stream needs a file entry”)
Example
var file = io.File.open(io.Path("big.bin")).value();
var entry = archive.TarEntry("big.bin");
entry.size = file.metadata().value().size;
tar.append_stream(entry, file);
finish
finish(): io.IoResult<void>
Writes the end-of-archive marker (two zero blocks). Later calls do nothing. It does not close the sink.
Returns
- an Ok result; an error when writing to the sink fails
Entry paths
safe_components: checking an entry’s path before extracting it, so an
archive cannot write outside the directory it is extracted into.
Source: runtime/archive/path.pep
safe_components
safe_components(path: String): String[]?
Splits the path of an archive entry into its components, or gives none
when extracting it could write outside the target directory (“zip
slip”): an absolute path, a .. component, a \ or a : (which
Windows would read as a separator or a drive letter). Empty and .
components are left out, so a//./b/ gives a and b.
TarReader.extract checks every entry with it, and so does the zip
package; a reader of another archive format should too.
Parameters
path— the entry’s path as stored, with/between components
Returns
- the components to join onto the target directory, or none for an unsafe path
Example
import archive;
assert(archive.safe_components("docs/./readme.md").value().count == 2, "safe");
assert(archive.safe_components("../etc/passwd").is_empty(), "climbs out");
assert(archive.safe_components("C:/Windows").is_empty(), "a drive");
The digest module
Cryptographic hashes: SHA-256, SHA-1 and BLAKE3, and HMAC.
Bring it in with import digest. Each hash has an incremental hasher
(update any number of times, then finish) and a one-shot function,
and both take bytes (u8[]) or a String (its UTF-8 bytes). A digest
is a u8[]; to_hex writes it as text.
- SHA-256:
Sha256,sha256 - SHA-1:
Sha1,sha1, for formats that need it, such as git object ids - BLAKE3:
Blake3,blake3, with keyed hashing, key derivation and output of any length - HMAC:
Hmac,hmac_sha256,hmac_sha1, andequal_constant_timeto check a MAC - Hexadecimal:
to_hexandfrom_hex
The implementations are portable Pepper with no link dependency, unless
the program is built with -D USE_LIBCRYPTO: then SHA-256 and SHA-1 call
OpenSSL’s libcrypto (CNG on Windows), which uses the CPU’s SHA
instructions. They are not hardened against timing side channels, which
matters only when hashing secrets.
For the hash tables of HashMap and HashSet, use hash<T> instead:
it is much faster, but neither cryptographic nor stable between
versions.
Example
import digest;
main(): int {
var id = digest.to_hex(digest.sha256("hello"));
assert(id == "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", "SHA-256");
return 0;
}
Source: runtime/digest/module.pep
SHA-256
SHA-256 (FIPS 180-4): 32-byte digests, as used to check downloads and sign data.
Source: runtime/digest/sha256.pep
Sha256
struct Sha256
An incremental SHA-256 hasher: feed it bytes with update, any number of
times and in pieces of any size, then read the 32-byte digest with
finish.
The digest of the input is the same however it was split between
update calls. finish resets the hasher, so it can hash a new input.
Example
import digest;
var hasher = digest.Sha256();
hasher.update("hello ");
hasher.update("world");
assert(hasher.finish_hex() == digest.to_hex(digest.sha256("hello world")), "same digest");
Constructor
Sha256()
construct()
Creates a hasher in the initial state, with no input.
Methods
reset
reset()
Discards the input given so far, returning the hasher to its initial state.
update
update(bytes: u8[])
Hashes bytes as the next part of the input.
Parameters
bytes— the bytes to add
update
update(text: String)
Hashes the UTF-8 bytes of text (without a terminating NUL) as the
next part of the input.
Parameters
text— the text to add
finish
finish(): u8[]
Returns the 32-byte digest of the input given since the hasher was created or last reset, then resets it.
Returns
- the digest, most significant byte first
finish_hex
finish_hex(): String
Returns the digest as finish does, written as 64 lowercase
hexadecimal digits.
Returns
- the digest in hexadecimal
sha256
sha256(bytes: u8[]): u8[]
Returns the SHA-256 digest of bytes.
Parameters
bytes— the input
Returns
- the 32-byte digest
Example
import digest;
var empty = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
assert(digest.to_hex(digest.sha256(Array<u8>())) == empty, "SHA-256 of nothing");
sha256
sha256(text: String): u8[]
Returns the SHA-256 digest of the UTF-8 bytes of text.
Parameters
text— the input
Returns
- the 32-byte digest
Example
import digest;
var abc = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
assert(digest.to_hex(digest.sha256("abc")) == abc, "FIPS 180-4 example");
SHA-1
SHA-1 (FIPS 180-4): 20-byte digests, as git uses for object ids. SHA-1 is broken for collision resistance; use it to read existing formats, and SHA-256 or BLAKE3 for anything new.
Source: runtime/digest/sha1.pep
Sha1
struct Sha1
An incremental SHA-1 hasher: feed it bytes with update, any number of
times and in pieces of any size, then read the 20-byte digest with
finish.
SHA-1 is not collision resistant: someone can make two inputs with the
same digest. Use it only where a format requires it, such as git object
ids. finish resets the hasher, so it can hash a new input.
Example
import digest;
# The id git gives an empty file ("blob 0" and a NUL byte).
u8[] nul = [0];
var hasher = digest.Sha1();
hasher.update("blob 0");
hasher.update(nul);
assert(hasher.finish_hex() == "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", "git blob id");
Constructor
Sha1()
construct()
Creates a hasher in the initial state, with no input.
Methods
reset
reset()
Discards the input given so far, returning the hasher to its initial state.
update
update(bytes: u8[])
Hashes bytes as the next part of the input.
Parameters
bytes— the bytes to add
update
update(text: String)
Hashes the UTF-8 bytes of text (without a terminating NUL) as the
next part of the input.
Parameters
text— the text to add
finish
finish(): u8[]
Returns the 20-byte digest of the input given since the hasher was created or last reset, then resets it.
Returns
- the digest, most significant byte first
finish_hex
finish_hex(): String
Returns the digest as finish does, written as 40 lowercase
hexadecimal digits.
Returns
- the digest in hexadecimal
sha1
sha1(bytes: u8[]): u8[]
Returns the SHA-1 digest of bytes.
Parameters
bytes— the input
Returns
- the 20-byte digest
sha1
sha1(text: String): u8[]
Returns the SHA-1 digest of the UTF-8 bytes of text.
Parameters
text— the input
Returns
- the 20-byte digest
Example
import digest;
assert(digest.to_hex(digest.sha1("abc")) == "a9993e364706816aba3e25717850c26c9cd0d89d", "FIPS 180-4 example");
BLAKE3
BLAKE3: 32-byte digests, faster than SHA-256 in hardware without SHA instructions, and the hash the compiler’s build cache keys objects with. Also a keyed hash (a MAC), key derivation, and output of any length.
Source: runtime/digest/blake3.pep
Blake3
struct Blake3
An incremental BLAKE3 hasher: feed it bytes with update, any number of
times and in pieces of any size, then read the 32-byte digest with
finish.
Blake3() is the default hash mode; Blake3.keyed and
Blake3.derive_key make hashers for the other two modes, and
finish(length) reads output of any length in each.
This is a portable implementation, one block at a time; it does not use
SIMD or threads, so it runs well below the speed of the official C and
Rust implementations. The digests are the same. finish resets the
hasher, so it can hash a new input.
Example
import digest;
var hasher = digest.Blake3();
hasher.update("hello ");
hasher.update("world");
assert(hasher.finish_hex() == digest.to_hex(digest.blake3("hello world")), "same digest");
Constructor
Blake3()
construct()
Creates a hasher in the initial state, with no input.
Static methods
keyed
static keyed(key: u8[]): Blake3
Returns a hasher for BLAKE3’s keyed mode, a MAC: only someone with
key can compute or check the digest of an input.
Parameters
key— the 32-byte secret key
Returns
- a hasher with no input, keyed with
key
Panics
- if
keyis not 32 bytes
Example
import digest;
var key = digest.blake3("a key made some other way");
var mac = digest.Blake3.keyed(key);
mac.update("message");
assert(mac.finish().count == 32, "a 32-byte MAC");
derive_key
static derive_key(context: String): Blake3
Returns a hasher for BLAKE3’s key derivation mode: the digest of its
input (the key material, such as a shared secret or a password hash)
is a key for the purpose that context names.
context should be a hard-coded string, unique to the application
and the purpose, such as
"example.com 2026-10-16 session tokens v1"; different contexts give
unrelated keys from the same material. Read the key with finish,
or finish(length) for one of another length.
Parameters
context— the application and purpose the key is for
Returns
- a hasher with no input, for keys in
context
Example
import digest;
var hasher = digest.Blake3.derive_key("example.com 2026-10-16 session tokens v1");
hasher.update("the shared secret");
var key = hasher.finish();
Methods
reset
reset()
Discards the input given so far, returning the hasher to its initial state.
update
update(bytes: u8[])
Hashes bytes as the next part of the input.
Parameters
bytes— the bytes to add
update_range
update_range(bytes: u8[], start: ulong, count: ulong)
Hashes count bytes of bytes, from index start, as the next part
of the input; those past the end of bytes are left out.
Parameters
bytes— the bytesstart— the index of the first byte to addcount— how many bytes to add
update
update(text: String)
Hashes the UTF-8 bytes of text (without a terminating NUL) as the
next part of the input.
Parameters
text— the text to add
finish
finish(): u8[]
Returns the 32-byte digest of the input given since the hasher was created or last reset, then resets it.
Returns
- the digest
finish
finish(length: ulong): u8[]
Returns length bytes of output for the input given since the
hasher was created or last reset, then resets it: BLAKE3’s extended
output. The first 32 bytes are the digest finish() returns, and a
shorter output is a prefix of a longer one.
Parameters
length— how many bytes to return
Returns
- the output
Example
import digest;
var long_output = digest.Blake3();
long_output.update("abc");
var bytes = long_output.finish(100);
assert(digest.to_hex(bytes).starts_with(digest.to_hex(digest.blake3("abc"))), "a prefix");
finish_hex
finish_hex(): String
Returns the digest as finish does, written as 64 lowercase
hexadecimal digits.
Returns
- the digest in hexadecimal
blake3
blake3(bytes: u8[]): u8[]
Returns the BLAKE3 digest of bytes.
Parameters
bytes— the input
Returns
- the 32-byte digest
Example
import digest;
var empty = "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262";
assert(digest.to_hex(digest.blake3(Array<u8>())) == empty, "BLAKE3 of nothing");
blake3
blake3(text: String): u8[]
Returns the BLAKE3 digest of the UTF-8 bytes of text.
Parameters
text— the input
Returns
- the 32-byte digest
HMAC
HMAC (RFC 2104): a message authentication code from a hash and a secret key, as webhook signatures, JWTs (HS256) and S3-style request signing use, and a constant-time comparison to check one.
Source: runtime/digest/hmac.pep
Hmac
struct Hmac<T>
An incremental HMAC hasher over the hash T: feed it the message with
update, any number of times and in pieces of any size, then read the
MAC with finish.
Only someone with the key can compute the MAC of a message, so a
receiver that shares the key can check that the message is authentic.
Compare a MAC you receive with equal_constant_time, not ==.
finish resets the hasher to the same key, so it can sign a new
message.
Type parameters
T— the hash:Sha256orSha1, or another hasher with a 64-byte block, a constructor taking no arguments, andupdateandfinishmethods
Example
import digest;
var mac = digest.Hmac<digest.Sha256>("key");
mac.update("The quick brown fox ");
mac.update("jumps over the lazy dog");
var expected = "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8";
assert(mac.finish_hex() == expected, "HMAC-SHA-256");
Constructors
Hmac()
construct(key: u8[])
Creates a hasher for MACs under key, with no input.
A key longer than 64 bytes is hashed with T first, as RFC 2104
says; one as long as T’s digest is the usual choice.
Parameters
key— the secret key
Hmac()
construct(key: String)
Creates a hasher for MACs under the UTF-8 bytes of key, with no
input.
Parameters
key— the secret key
Methods
reset
reset()
Discards the input given so far, returning the hasher to its initial state under the same key.
update
update(bytes: u8[])
Adds bytes as the next part of the message.
Parameters
bytes— the bytes to add
update
update(text: String)
Adds the UTF-8 bytes of text (without a terminating NUL) as the
next part of the message.
Parameters
text— the text to add
finish
finish(): u8[]
Returns the MAC of the message given since the hasher was created or last reset, then resets it.
Returns
- the MAC, as long as
T’s digest
finish_hex
finish_hex(): String
Returns the MAC as finish does, written as lowercase hexadecimal
digits.
Returns
- the MAC in hexadecimal
hmac_sha256
hmac_sha256(key: u8[], message: u8[]): u8[]
Returns the HMAC-SHA-256 of message under key.
Parameters
key— the secret keymessage— the message to sign
Returns
- the 32-byte MAC
hmac_sha256
hmac_sha256(key: String, message: String): u8[]
Returns the HMAC-SHA-256 of the UTF-8 bytes of message under the UTF-8
bytes of key.
Parameters
key— the secret keymessage— the message to sign
Returns
- the 32-byte MAC
Example
import digest;
var mac = digest.hmac_sha256("key", "The quick brown fox jumps over the lazy dog");
var expected = "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8";
assert(digest.to_hex(mac) == expected, "HMAC-SHA-256");
hmac_sha1
hmac_sha1(key: u8[], message: u8[]): u8[]
Returns the HMAC-SHA-1 of message under key, for formats that
require it. HMAC-SHA-1 is still secure as a MAC, though SHA-1 alone is
not.
Parameters
key— the secret keymessage— the message to sign
Returns
- the 20-byte MAC
hmac_sha1
hmac_sha1(key: String, message: String): u8[]
Returns the HMAC-SHA-1 of the UTF-8 bytes of message under the UTF-8
bytes of key.
Parameters
key— the secret keymessage— the message to sign
Returns
- the 20-byte MAC
equal_constant_time
equal_constant_time(a: u8[], b: u8[]): bool
Returns whether a and b hold the same bytes, taking the same time
wherever they first differ.
Check a MAC with this rather than ==, which stops at the first
difference: timing it would let an attacker find a valid MAC a byte at a
time. The lengths are not secret, so arrays of different lengths return
false at once.
Parameters
a— the first bytes, such as the MAC computedb— the second bytes, such as the MAC received
Returns
trueifaandbhave the same length and bytes
Example
import digest;
var mac = digest.hmac_sha256("key", "message");
assert(digest.equal_constant_time(mac, digest.hmac_sha256("key", "message")), "a valid MAC");
Hexadecimal
Hexadecimal text for digests, and back.
Source: runtime/digest/hex.pep
to_hex
to_hex(bytes: u8[]): String
Returns bytes as lowercase hexadecimal text, two digits per byte, the
way checksums and git object ids are usually written.
Parameters
bytes— the bytes to write, such as a digest fromfinish
Returns
- a String of
2 * bytes.countcharacters from0-9a-f
Example
import digest;
assert(digest.to_hex([0, 15, 255]) == "000fff", "two digits per byte");
from_hex
from_hex(text: String): Result<u8[]>
Returns the bytes that hexadecimal text stands for, two digits per
byte, the reverse of to_hex. Upper- and lowercase digits are both
accepted.
Parameters
text— hexadecimal text, such as a checksum
Returns
- the bytes, or an error when
texthas an odd length or a character that is not a hexadecimal digit
Example
import digest;
var bytes = digest.from_hex("000fFF").value();
assert(bytes.count == 3 && bytes[2] == 255, "two digits per byte");
assert(digest.from_hex("abc").is_err(), "an odd length");
The random module
Random numbers: bytes from the operating system’s secure source, and
Generator, a fast seedable generator for everything else.
random is a module: bring it in with import random (and write
random.next_int(1..>6)) or import names directly with
import [Generator, shuffle] from random.
- Entropy:
bytes,fillandsecure_ulong, from the operating system, for keys, tokens and identifiers - Generators:
Generator, seeded from entropy or from a fixed seed, andnext_int,next_double,next_bool,chooseandshuffleon this thread’s own generator
Example
import random;
main(): int {
var die = random.next_int(1..>6); # 1 to 6
var names = ["ada", "grace", "edsger"];
random.shuffle(names);
var key = random.bytes(32); # 32 secure bytes
var replay = random.Generator(42); # the same numbers every run
print(replay.next_double());
return 0;
}
Source: runtime/random/module.pep
Entropy
Secure random bytes from the operating system: getentropy on Linux,
Android, macOS, iOS and WASI, and BCryptGenRandom on Windows. They are
unpredictable, so they suit keys, tokens and identifiers, but each call
asks the kernel; a Generator is much faster for everything else.
Source: runtime/random/entropy.pep
Generators
Generator, a fast pseudo-random generator, and functions that use this
thread’s own generator, so code that only needs a random number need not
pass one around.
The algorithm is xoshiro256** (Blackman and Vigna): 256 bits of state,
a period of 2^256 - 1, and statistically strong output. It is not secure:
anyone who sees enough output can predict the rest, so keys and tokens
come from random.bytes instead.
Source: runtime/random/generator.pep
The base64 module
Base64 (RFC 4648): bytes written as text, for HTTP basic authentication, data URIs, JSON Web Tokens, PEM files and WebSocket handshakes.
Bring it in with import base64. encode writes the standard alphabet
(A-Z a-z 0-9 + /) with = padding; pass url_safe for the URL and
file name alphabet (- and _ in place of + and /), and
padded = false to leave the padding off, as JSON Web Tokens do.
decode takes text with or without padding, and rejects anything else
that is not base64, including whitespace.
Example
import base64;
main(): int {
var text = base64.encode("user:secret".bytes());
assert(text == "dXNlcjpzZWNyZXQ=", "standard, padded");
var bytes = base64.decode(text).value();
assert(bytes.count == 11 && bytes[0] == 'u', "round trip");
assert(base64.encode([251, 255], true, false) == "-_8", "URL-safe, unpadded");
return 0;
}
Source: runtime/base64/module.pep
encode
encode(bytes: u8[], url_safe: bool = false, padded: bool = true): String
Returns bytes as base64 text: four characters for each three bytes.
Parameters
bytes— the bytes to writeurl_safe— true for the URL and file name alphabet, with-and_in place of+and/padded— false to leave off the=that pads the text to a multiple of four characters
Returns
- the base64 text, empty when
bytesis
Example
assert(base64.encode("hi".bytes()) == "aGk=", "padded");
assert(base64.encode("hi".bytes(), false, false) == "aGk", "unpadded");
decode
decode(text: String, url_safe: bool = false): Result<u8[]>
Returns the bytes that base64 text stands for.
The padding is optional, but when present it must complete the last group of four. The decoding is strict, so each text has one meaning: a character outside the alphabet (whitespace included), a length no encoding gives, or unused bits that are not zero is an error.
Parameters
text— base64 text, such asaGk=oraGkurl_safe— true for the URL and file name alphabet, with-and_in place of+and/
Returns
- the decoded bytes, or an error naming what is not base64
Example
var bytes = base64.decode("aGk").value();
assert(bytes.count == 2 && bytes[1] == 'i', "unpadded");
assert(base64.decode("a b=").is_err(), "a space");
The uuid module
UUIDs (RFC 9562): 128-bit identifiers, written as
0190163d-8694-739b-aea5-966c26f8ad91.
Bring it in with import uuid (or import [Uuid] from uuid). Uuid.v4()
makes a random one; Uuid.v7() puts the time first, so ids made later
sort later, which keeps database indexes compact. Both draw their random
bits from the operating system’s secure source (random.bytes).
Example
import uuid;
main(): int {
var id = uuid.Uuid.v4();
var text = id.to_string(); # such as 9b2c3c1e-...
assert(uuid.Uuid.parse(text).value() == id, "round trip");
assert(id.version() == 4, "random");
return 0;
}
Source: runtime/uuid/module.pep
Uuid
[Value] struct Uuid
A UUID, as a value struct of two 64-bit
halves: == compares them and hash hashes them, so a Uuid works as
a map key.
Example
var id = Uuid.parse("0190163D-8694-739B-AEA5-966C26F8AD91").value();
assert(id.version() == 7, "the version digit");
assert(id.to_string() == "0190163d-8694-739b-aea5-966c26f8ad91", "lowercase");
Fields
high
var high: ulong
The first 8 bytes, the first byte in the top bits.
low
var low: ulong
The last 8 bytes, the first of them in the top bits.
Constructor
Uuid()
construct(high: ulong, low: ulong)
Makes the UUID with these halves; Uuid(0, 0) is the nil UUID.
Parameters
high— the first 8 bytes, the first byte in the top bitslow— the last 8 bytes, the first of them in the top bits
Static methods
v4
static v4(): Uuid
Returns a new random UUID (version 4): 122 random bits.
Returns
- the UUID
Panics
- if the operating system cannot give random bytes, which only a broken system does
v7
static v7(): Uuid
Returns a new time-ordered UUID (version 7): the wall clock’s Unix time in milliseconds in the first 48 bits, then 74 random bits. Ids made in different milliseconds sort by time, both as values and as text; ids made in the same millisecond are in no particular order.
Returns
- the UUID
Panics
- if the operating system cannot give random bytes, which only a broken system does
parse
static parse(text: String): Result<Uuid>
Returns the UUID that text writes: 32 hexadecimal digits, either
case, in groups of 8, 4, 4, 4 and 12 separated by -.
Parameters
text— the UUID as text, such as0190163d-8694-739b-aea5-966c26f8ad91
Returns
- the UUID, or an error when
textis not in that form
Methods
version
version(): int
Returns the version: 4 for v4, 7 for v7, 0 for the nil UUID.
Returns
- the version digit, from 0 to 15
bytes
bytes(): u8[]
Returns the UUID’s 16 bytes, high first, as databases and binary
formats store it.
Returns
- 16 bytes
to_string
to_string(): String
Returns the UUID as 32 lowercase hexadecimal digits in groups of 8,
4, 4, 4 and 12, such as 0190163d-8694-739b-aea5-966c26f8ad91.
Returns
- the UUID as text, 36 characters
The cli module
Command-line parsing: declare a program’s options and arguments, then parse them, with the help, version and error messages generated from the declarations.
Bring it in with import cli. The raw arguments stay where they are,
in main(args: String[]) and process.arguments().
- Parser:
Parser, and the handles its declarations return:Flag,Argument,ArgumentList; andclosest, which picks the name a misspelled one most likely meant - Completions:
Shell, the shellsParser.completionswrites a completion script for - Options:
OptionandPositional, the attributes that shape whatParser.add_optionsdeclares from a struct’s fields
Example
import cli;
main(args: String[]): int {
var parser = cli.Parser("greet", "Greets someone");
var loud = parser.flag("loud", "l", "shout the greeting");
var name = parser.positional("name", "who to greet", required = false, default_value = "world");
if (!parser.parse(args)) {
return 2;
}
var greeting = $"hello, {name.value()}";
print(loud.is_set ? greeting.upper() : greeting);
return 0;
}
Source: runtime/cli/module.pep
Parser
Parser and the handles its declarations return: Flag, Argument and ArgumentList.
Source: runtime/cli/parser.pep
Parser
struct Parser
Declares a program’s command-line interface and parses its arguments.
Declare each option and positional argument with flag, option,
multiple, positional and positionals; each returns a handle that
reads its value once parse has run. parse also writes the help
(--help), the version (--version) and clang-style errors, all from
the declarations.
Options have a long name (--output), a one-letter short name
(-o), or both. A value follows as --output a.out, --output=a.out,
-o a.out or -oa.out; short flags combine (-vq is -v -q), and
-- ends the options, so every argument after it is positional. A value
is taken as it is, even when it starts with -. An option declared
with an implicit_value takes a value only when it is attached
(--emit-ast=json, -O3), and the implicit one when it is not. When
an option that takes one value is given twice, the last value wins.
Subcommands (git add, pep-package update) are parsers of their own,
made with command. Options before the command’s name belong to this
parser, and everything after it to the command’s.
Example
import cli;
main(args: String[]): int {
var parser = cli.Parser("pepper", "Compiles a Pepper program", version = "0.2.1");
var output = parser.option("output", "o", "the executable to write", default_value = "a.out");
var imports = parser.multiple("import-path", "I", "a directory to search for modules");
var verbose = parser.flag("verbose", "v", "say what is happening");
var source = parser.positional("source", "the program's entry file");
if (!parser.parse(args)) {
return 2;
}
print($"{source.value()} -> {output.value()}");
return 0;
}
Properties
is_selected
var is_selected: bool { get; }
Whether the last parse reached this parser: whether this command
was given. The top-level parser is selected whenever its arguments
were parsed.
Constructor
Parser()
construct(name: String, about: String = "", version: String = "")
Makes a parser for the program name.
Parameters
name— the program’s name, as the help and errors show itabout— a sentence on what the program does, the help’s first lineversion— the program’s version; when not empty,--versionand-Vprintname version
Methods
flag
flag(name: String, short_name: String = "", help: String = "", global: bool = false): Flag
Declares an option that takes no value, such as --verbose.
Parameters
name— the long name, without--, or empty for an option with only a short name (-I)short_name— the one-letter short name, without-, or empty for nonehelp— what the option does, for the helpglobal— whether the flag may also be given after the name of a command, at any depth (pkg add -v xas well aspkg -v add x)
Returns
- the handle that tells whether the flag was given
Panics
- if
nameorshort_nameis invalid or already declared, here, as a global option of a parser this one is a command of, or (for a global flag) in one of its commands
option
option( name: String, short_name: String = "", help: String = "", value_name: String = "", default_value: String = "", required: bool = false, global: bool = false, implicit_value: String = "" ): Argument<String>
Declares an option that takes one String value, such as
--output a.out. option<T> declares one whose value is a number,
a path or an enum case.
Parameters
name— the long name, without--, or empty for an option with only a short name (-I)short_name— the one-letter short name, without-, or empty for nonehelp— what the option sets, for the helpvalue_name— what the help calls the value; empty for the long name in capitals (OUTPUT)default_value— the value when the option is not given; empty for nonerequired— whether leaving the option out is an errorglobal— whether the option may also be given after the name of a command, at any depthimplicit_value— the value when the option is given without one; when not empty, a value must be attached (--name=value,-xvalue) and the next argument is never taken as one
Returns
- the handle that reads the value
Panics
- if
nameorshort_nameis invalid or already declared
option
option<T>( name: String, short_name: String = "", help: String = "", value_name: String = "", default_value: String = "", required: bool = false, global: bool = false, implicit_value: String = "" ): Argument<T>
Declares an option that takes one value of type T, such as
--jobs 4 for option<int>. A value that does not read as a T
fails the parse: invalid value 'x' for '--jobs': expected an integer.
T may be String, an integer type, float, double, io.Path,
or an enum whose cases carry no values; a case is written in kebab
case (DebugInfo is debug-info), and the help lists them.
Type parameters
T— the type of the value
Parameters
name— the long name, without--, or empty for an option with only a short name (-I)short_name— the one-letter short name, without-, or empty for nonehelp— what the option sets, for the helpvalue_name— what the help calls the value; empty for the long name in capitals (JOBS)default_value— the value when the option is not given, written as on the command line ("4"); empty for nonerequired— whether leaving the option out is an errorglobal— whether the option may also be given after the name of a command, at any depthimplicit_value— the value when the option is given without one, written as on the command line (-Oalone as-O2); when not empty, a value must be attached (--name=value,-xvalue) and the next argument is never taken as one
Returns
- the handle that reads the value
Panics
- if
nameorshort_nameis invalid or already declared, or ifdefault_valueorimplicit_valuedoes not read as aT
Example
var jobs = parser.option<int>("jobs", "j", "how many at once", default_value = "4");
multiple
multiple( name: String, short_name: String = "", help: String = "", value_name: String = "", required: bool = false, global: bool = false ): ArgumentList<String>
Declares an option that may be given any number of times, each with a
String value, such as -I src -I lib. multiple<T> declares one
whose values are numbers, paths or enum cases.
Parameters
name— the long name, without--, or empty for an option with only a short name (-I)short_name— the one-letter short name, without-, or empty for nonehelp— what the option adds, for the helpvalue_name— what the help calls the value; empty for the long name in capitals (IMPORT_PATH)required— whether leaving the option out is an errorglobal— whether the option may also be given after the name of a command, at any depth
Returns
- the handle that reads the values, in the order given
Panics
- if
nameorshort_nameis invalid or already declared
multiple
multiple<T>( name: String, short_name: String = "", help: String = "", value_name: String = "", required: bool = false, global: bool = false ): ArgumentList<T>
Declares an option that may be given any number of times, each with a
value of type T, such as -I src -I lib for multiple<io.Path>.
T is one of the types option<T> takes.
Type parameters
T— the type of each value
Parameters
name— the long name, without--, or empty for an option with only a short name (-I)short_name— the one-letter short name, without-, or empty for nonehelp— what the option adds, for the helpvalue_name— what the help calls the value; empty for the long name in capitals (IMPORT_PATH)required— whether leaving the option out is an errorglobal— whether the option may also be given after the name of a command, at any depth
Returns
- the handle that reads the values, in the order given
Panics
- if
nameorshort_nameis invalid or already declared
positional
positional(name: String, help: String = "", required: bool = true, default_value: String = ""): Argument<String>
Declares the next positional argument, with a String value.
Positional arguments are filled in the order they are declared.
positional<T> declares one whose value is a number, a path or an
enum case.
Parameters
name— what the help and errors call ithelp— what it is, for the helprequired— whether leaving it out is an errordefault_value— the value when it is left out; empty for none
Returns
- the handle that reads the value
Panics
- if
nameis empty or already declared, if a required argument follows an optional one, if it followspositionals, or if the parser has commands
positional
positional<T>(name: String, help: String = "", required: bool = true, default_value: String = ""): Argument<T>
Declares the next positional argument, with a value of type T.
T is one of the types option<T> takes.
Type parameters
T— the type of the value
Parameters
name— what the help and errors call ithelp— what it is, for the helprequired— whether leaving it out is an errordefault_value— the value when it is left out, written as on the command line; empty for none
Returns
- the handle that reads the value
Panics
- if
nameis empty or already declared, if a required argument follows an optional one, if it followspositionals, if the parser has commands, or ifdefault_valuedoes not read as aT
positionals
positionals(name: String, help: String = "", required: bool = false): ArgumentList<String>
Declares a positional argument that takes every remaining value, such
as a list of files. It must be the last positional argument.
positionals<T> declares one whose values are numbers, paths or enum
cases.
Parameters
name— what the help and errors call ithelp— what the values are, for the helprequired— whether at least one value must be given
Returns
- the handle that reads the values, in the order given
Panics
- if
nameis empty or already declared, if it followspositionals, or if the parser has commands
positionals
positionals<T>(name: String, help: String = "", required: bool = false): ArgumentList<T>
Declares a positional argument that takes every remaining value, each
of type T, such as a list of files for positionals<io.Path>. It
must be the last positional argument. T is one of the types
option<T> takes.
Type parameters
T— the type of each value
Parameters
name— what the help and errors call ithelp— what the values are, for the helprequired— whether at least one value must be given
Returns
- the handle that reads the values, in the order given
Panics
- if
nameis empty or already declared, if it followspositionals, or if the parser has commands
command
command(name: String, about: String = ""): Parser
Declares a subcommand, such as add in pep-package add NAME, and
returns its parser, on which to declare its own options and
arguments.
A parser with commands requires one: the first argument that is not
an option names it, and the arguments after it go to its parser.
After parse, is_selected on the command’s parser tells whether
it was the one given.
Parameters
name— the command’s nameabout— what the command does, for the help
Returns
- the command’s parser
Panics
- if
nameis empty, starts with-or is already declared, or if this parser has positional arguments
add_options
add_options<T>(options: T)
Declares an option or positional argument for each public field of
options, and has every successful parse store the values in those
fields.
A field’s type picks what it declares: bool is a flag, String an
option with one value, String? one that may be left out, and
String[] one that may be given many times. An option is named after
its field, with each _ written - (import_path is
--import-path); [cli.Option] on a field gives it a short name,
help, another name, or makes it required. [cli.Positional] makes a
field the next positional argument instead.
A String field that holds a value when this is called keeps it as
the default; an empty one has no default, so leaving the option out
leaves it empty, or is an error for a positional argument. Every
parse sets every field: a flag to whether it was given, a list to the
values given.
The declarations are the same as the builder methods make, so the help and errors read the same, and the two styles mix on one parser.
Type parameters
T— the options struct
Parameters
options— the struct whose fields to declare and fill in
Example
import cli;
struct Options {
[cli.Option("o", "the executable to write")]
var output: String = "a.out";
[cli.Option("v", "say what is happening")]
var verbose: bool;
[cli.Positional("the program's entry file")]
var source: String;
}
main(args: String[]): int {
var options = Options();
var parser = cli.Parser("pepper", "Compiles a Pepper program");
parser.add_options(options);
if (!parser.parse(args)) {
return 2;
}
print($"{options.source} -> {options.output}");
return 0;
}
usage
usage(): String
Returns the one-line usage, such as
usage: pepper [OPTIONS] <source>.
Returns
- the usage line, without a line break
help
help(): String
Returns the help that --help prints: what the program does, the
usage, and a line for each argument, option and command.
Returns
- the help text, ending in a line break
completions
completions(shell: Shell): String
Returns a script that completes this program’s command line in
shell: its options, commands and enum choices, and file names
where a value is a String or an io.Path. A program usually
prints it from a command of its own (pkg completions bash), for
the user to install where Shell’s cases say.
Call it on the program’s parser, after the declarations.
Parameters
shell— the shell the script is for
Returns
- the script, ending in a line break
Example
var parser = cli.Parser("pkg");
var shell = parser.option<cli.Shell>("completions", help = "print a completion script");
if (!parser.parse(args)) {
return 2;
}
if (shell.is_set) {
print(parser.completions(shell.value()));
}
try_parse
try_parse(args: String[]): Result<String?>
Parses args and fills in every handle, or reports the problem.
Writes nothing. Use it to handle errors, help and the version
yourself; parse does it the usual way.
Parameters
args— the arguments asmainreceives them: the program’s name, then its arguments
Returns
- some text when
--helpor--versionwas given (the program should print it and exit with 0), none when the arguments were parsed, or an error when they are wrong, with a clang-style message such asmissing value for '-o'
parse
parse(args: String[]): bool
Parses args and fills in every handle.
For --help or --version it prints the text to standard output and
ends the program with status 0. When the arguments are wrong it
writes the error, the usage and a hint to standard error, such as
pepper: error: missing value for '-o'
usage: pepper [OPTIONS] <source>
Run 'pepper --help' for more information.
and returns false; exit with status 2, as the example does.
Parameters
args— the arguments asmainreceives them: the program’s name, then its arguments
Returns
- true when the arguments were parsed, false after an error
usage_error
usage_error(message: String): int
Writes a usage error found after parse, such as two options that
cannot be used together, as parse writes one: the error, this
parser’s usage and a hint, to standard error.
Parameters
message— what is wrong, clang-style ('--test' cannot be used with '--lib')
Returns
- 2, the status a program exits with after a usage error
Example
if (test.is_set and library.is_set) {
return parser.usage_error("'--test' cannot be used with '--lib'");
}
closest
closest(name: String, candidates: Array<String>): String?
Returns the candidate closest to a misspelled name, for a “did you
mean” suggestion: the one the fewest single-byte insertions, deletions
and substitutions turn name into, when that is at most 2. Of equally
close candidates the first wins.
Parameters
name— the name that matched nothingcandidates— the names it could have meant
Returns
- the closest candidate, or none when none is within 2 edits
Example
var names = Array<String>();
names.append("build");
names.append("fmt");
var best = cli.closest("buidl", names); # some("build")
Flag
struct Flag
An option that takes no value, from Parser.flag.
Properties
is_set
var is_set: bool { get; }
Whether the flag was given at least once.
occurrences
var occurrences: int { get; }
How many times the flag was given, so -vvv can mean more than -v.
Constructor
Flag()
construct(argument: _CliArgument)
Wraps a declaration. Parser.flag makes flags; this constructor is
for the runtime.
Parameters
argument— the declaration
Argument
struct Argument<T>
An option or positional argument that takes one value, from
Parser.option or Parser.positional.
Type parameters
T— the type of the value:String, or whatoption<T>orpositional<T>declared
Properties
is_set
var is_set: bool { get; }
Whether a value was given on the command line; false when the argument was left out, even when it has a default.
Constructor
Argument()
construct(argument: _CliArgument)
Wraps a declaration. Parser.option and Parser.positional make
arguments; this constructor is for the runtime.
Parameters
argument— the declaration
Methods
value
value(): T
Returns the value given, or the default when it was left out.
Returns
- the value
Panics
- if the argument was left out and has no default; check
is_setor usevalue_orfor an optional argument
value_or
value_or(fallback: T): T
Returns the value given, or fallback when it was left out.
Parameters
fallback— the value to use when the argument was left out
Returns
- the value given, else the declared default, else
fallback
ArgumentList
struct ArgumentList<T>
An option that may be given many times, or a positional argument that
takes every remaining value, from Parser.multiple or
Parser.positionals.
Type parameters
T— the type of each value:String, or whatmultiple<T>orpositionals<T>declared
Properties
is_set
var is_set: bool { get; }
Whether at least one value was given.
Constructor
ArgumentList()
construct(argument: _CliArgument)
Wraps a declaration. Parser.multiple and Parser.positionals make
argument lists; this constructor is for the runtime.
Parameters
argument— the declaration
Methods
values
values(): Array<T>
Returns the values given, in command-line order.
Returns
- a new array of the values, empty when none was given
Options
The attributes that shape the options Parser.add_options declares
from a struct’s fields.
Source: runtime/cli/options.pep
Option
[Attribute] struct Option
Sets how a field of an options struct is declared as an option: its short name, its help, and whether it is required or global. A field without it is still an option, named after the field.
Example
import cli;
struct Options {
[cli.Option("o", "the executable to write")]
var output: String = "a.out";
[cli.Option("v", "say what is happening")]
var verbose: bool;
}
Fields
short_name
const short_name: String = ""
The one-letter short name, without -, or empty for none.
help
const help: String = ""
What the option does, for the help.
name
const name: String = ""
The long name, without --; empty for the field’s name with each
_ written - (import_path is --import-path).
value_name
const value_name: String = ""
What the help calls the value; empty for the long name in capitals.
required
const required: bool = false
Whether leaving the option out is an error.
global
const global: bool = false
Whether the option may also be given after the name of a command, at any depth.
Positional
[Attribute] struct Positional
Declares a field of an options struct as the next positional argument
instead of an option. A T field takes one value, required unless the
field starts with a value (its default; an empty String or a null
io.Path is none); a T? field takes an optional one; a T[] field
takes every value that is left.
Example
import cli;
struct Options {
[cli.Positional("the program's entry file")]
var source: String;
}
Fields
help
const help: String = ""
What the argument is, for the help.
name
const name: String = ""
What the help and errors call it; empty for the field’s name.
required
const required: bool = false
Whether leaving it out is an error, even when the field has a
default; for a T[] field, whether at least one value must be
given. A T? field ignores it.
Completions
Shell completion scripts generated from a Parser: Shell names the
shells, and Parser.completions writes the script.
Source: runtime/cli/completions.pep
Shell
enum Shell
A shell that Parser.completions writes a completion script for.
Cases
Bash
Bash
GNU Bash: source the script, or put it in
~/.local/share/bash-completion/completions/NAME.
Zsh
Zsh
Zsh: put the script in a directory on $fpath as _NAME.
Fish
Fish
fish: put the script in ~/.config/fish/completions/NAME.fish.
Powershell
Powershell
PowerShell (powershell on a command line): dot-source the script from your profile.
The http module
HTTP/1.1 and HTTP/2 clients and servers, over TCP or TLS.
Bring it in with import http. A Client sends a Request to an
http:// or https:// URL and returns the Response; a Server
accepts connections on every worker of async.run and answers each
request with a handler function. Bodies are read into memory, or
streamed through an io.Reader (Client.open, set_reader). Over TLS
the client and server choose HTTP/2 through ALPN when both have it, with
the same Request and Response types.
- Messages:
Headers,Request, andResponse - Client:
Client,SessionandBodyReader - Server:
Serverandreason_phrase
Example
import async;
import http;
import io;
main(): int {
async.run(() => {
var server = http.Server.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var port = server.local_address().value().port();
async.spawn<void>(() => {
server.serve((request) => {
return http.Response(200).set_text("hello");
});
});
var response = http.Client().get("http://127.0.0.1:" + to_string(port) + "/").value();
assert(response.text() == "hello", "served");
});
return 0;
}
Source: runtime/http/module.pep
Requests and responses
HTTP messages: Headers, Request and Response, the values a
Client sends and receives and a Server handler takes and returns.
Source: runtime/http/message.pep
Headers
struct Headers
HTTP header fields, by name. Names ignore ASCII case: they are stored
lowercased, so Content-Type and content-type are the same field. A
name may hold several values, kept in the order added. Different names
have no defined order, as HTTP allows.
Example
var headers = http.Headers();
headers.add("Accept", "text/html");
headers["accept"] = "application/json"; # replaces it
assert(headers["ACCEPT"] == "application/json", "one value");
Properties
count
var count: ulong { get; }
The number of field names.
Constructor
Headers()
construct()
Creates an empty set of fields.
Methods
names
names(): String[]
Returns the field names, lowercased, in no defined order.
Returns
- a new array with one entry per name
values
values(name: String): String[]
Returns every value of the field named name, in the order added.
Parameters
name— the field name, in any case
Returns
- a new array of the values; empty when no field has that name
add
add(name: String, value: String)
Adds a value to the field named name, keeping any it has.
Parameters
name— the field name, in any casevalue— the field value
set
set(name: String, value: String)
Replaces every value of the field named name with value.
Parameters
name— the field name, in any casevalue— the field value
get
get(name: String): String?
Returns the first value of the field named name.
Parameters
name— the field name, in any case
Returns
- the value, or none when no field has that name
has
has(name: String): bool
Reports whether a field is named name.
Parameters
name— the field name, in any case
Returns
- true if a field has that name
remove
remove(name: String)
Removes the field named name and all its values.
Parameters
name— the field name, in any case
operator []
[](name: String): String
Reads headers[name]: the first value of the field named name, or
an empty string when there is none. Use get to tell an empty value
from a missing field.
Parameters
name— the field name, in any case
Returns
- the first value, or
""
operator []
[](name: String, value: String): String
Writes headers[name] = value: replaces every value of the field, as
set does.
Parameters
name— the field name, in any casevalue— the field value
Returns
value, the value of the assignment expression
Request
struct Request
An HTTP request.
A Client sends one: build it with a method and an absolute URL, then
add headers and a body. A Server handler receives one, with url set to
the target from the request line (/path?query) and the body read in
full.
Example
var request = http.Request("POST", "http://localhost:8080/items").set_text("name=pepper");
request.headers["Content-Type"] = "application/x-www-form-urlencoded";
Fields
method
var method: String
The method, such as GET or POST.
url
var url: String
For a client, the absolute URL (https://host/path?query); for a
server handler, the request target (/path?query).
headers
var headers: Headers
The header fields. A client adds Host, Content-Length (or
Transfer-Encoding) and Connection itself.
Constructor
Request()
construct(method: String = "GET", url: String = "/")
Creates a request with no headers and no body.
Parameters
method— the method, such asGETurl— the absolute URL to send it to
Methods
set_text
set_text(text: String): Request
Sets the body to the UTF-8 bytes of text.
Parameters
text— the body
Returns
- this request
set_body
set_body(data: u8[]): Request
Sets the body to data, which may be any bytes (see body).
Parameters
data— the body’s bytes
Returns
- this request
set_reader
set_reader(source: io.Reader, length: long = -1): Request
Sets the body to the bytes source gives, read as the request is sent
rather than held in memory: a large file, or the output of a
compress.CompressReader. With a length the client sends
Content-Length; without one it sends the body chunked
(Transfer-Encoding: chunked), which every HTTP/1.1 server accepts.
The source is read once, so a client cannot send the request again (to follow a redirect, for instance).
Parameters
source— the stream the body is read from, to its endlength— the number of bytessourcegives, or -1 when it is not known
Returns
- this request
Example
var file = io.File.open(io.Path("upload.bin")).value();
var request = http.Request("PUT", "http://localhost:8080/files/upload.bin").set_reader(file);
uri
uri(): Result<Uri>
Parses url as a URI reference: an absolute URL for a request a
client sends, or the target (/items?page=2) for one a server
received.
Returns
- the parsed URL, or an error when
urlis not a valid URI reference (seeUri.parse)
path
path(): String
Returns the path of url, with its percent-escapes: / when the path
is empty.
Returns
- the path, such as
/items, or an empty string whenurlis not a valid URI reference
query
query(): String
Returns the query of url: the text after ?, with its
percent-escapes (see Uri.decode).
Returns
- the query without the
?, or an empty string for none or whenurlis not a valid URI reference
body
body(): u8[]
Returns the body’s bytes, as they were sent or received: use it for a
binary body. The array is the body itself, not a copy. It is empty
when the body comes from set_reader.
Returns
- the body; empty for none
text
text(): String
Returns the body as text, decoded as UTF-8. For a binary body, call
body() instead: here each byte that is not valid UTF-8 becomes
U+FFFD, the replacement character.
Returns
- the body’s bytes decoded as a String
Response
struct Response
An HTTP response.
Client.send returns one with the body read in full; Client.open
returns one whose body is read as it arrives, through reader(). A
Server handler returns one: build it with a status, then add headers
and a body, in memory (set_text, set_body) or streamed
(set_reader). The server adds Content-Length (or
Transfer-Encoding: chunked) itself.
Example
handle(request: http.Request): http.Response {
var response = http.Response(200).set_text("hello");
response.headers["Content-Type"] = "text/plain";
return response;
}
Fields
status
var status: int
The status code, such as 200 or 404.
reason
var reason: String
The reason phrase, such as OK; the server sends the standard one
for the status when this is empty.
headers
var headers: Headers
The header fields.
url
var url: String
For a response a Client returns, the URL it came from: the
request’s, or after redirects the last one’s. Empty otherwise.
Constructor
Response()
construct(status: int = 200)
Creates a response with no headers and no body.
Parameters
status— the status code
Methods
set_text
set_text(text: String): Response
Sets the body to the UTF-8 bytes of text.
Parameters
text— the body
Returns
- this response
set_body
set_body(data: u8[]): Response
Sets the body to data, which may be any bytes (see body).
Parameters
data— the body’s bytes
Returns
- this response
set_reader
set_reader(source: io.Reader, length: long = -1): Response
Sets the body to the bytes source gives, which the server reads and
sends as it goes rather than holding them in memory. With a length
the server sends Content-Length; without one it sends the body
chunked to an HTTP/1.1 client, and to an HTTP/1.0 client sends it
unframed and closes the connection after it.
Parameters
source— the stream the body is read from, to its endlength— the number of bytessourcegives, or -1 when it is not known
Returns
- this response
Example
download(request: http.Request): http.Response {
var file = io.File.open(io.Path("large.bin")).value();
return http.Response(200).set_reader(file);
}
body
body(): u8[]
Returns the body’s bytes, as they were sent or received: use it for a
binary body. The array is the body itself, not a copy. It is empty for
a response from Client.open, whose body is read through reader(),
and for one whose body comes from set_reader.
Returns
- the body; empty for none
reader
reader(): io.Reader
Returns the body as a stream. For a response from Client.open it is
the connection’s BodyReader, which reads the body as it arrives and
ends where the body ends; read it once. For a response whose body
comes from set_reader it is that source, and otherwise an
io.ByteReader over body().
Returns
- the stream of the body’s bytes
Example
var response = http.Client().open(http.Request("GET", url)).value();
var file = io.File.create(io.Path("download.bin")).value();
for (var chunk in response.reader().chunks(65536)) {
file.write_all(chunk.value());
}
text
text(): String
Returns the body as text, decoded as UTF-8. For a binary body, call
body() instead: here each byte that is not valid UTF-8 becomes
U+FFFD, the replacement character.
Returns
- the body’s bytes decoded as a String
is_success
is_success(): bool
Reports whether the status is 2xx.
Returns
- true for a status from 200 to 299
Client
The HTTP client: Client sends a Request over HTTP or HTTPS and
returns the Response.
Source: runtime/http/client.pep
MAX_RESPONSE_BODY
const MAX_RESPONSE_BODY: ulong = 67108864
The largest response body a Client reads: 64 MiB.
Client
[Shared] struct Client
Sends HTTP/1.1 and HTTP/2 requests and reads their responses.
Each request opens a new connection (plain TCP for http://, TLS for
https://), sends the request with Connection: close, and reads the
response: send reads the whole body into memory, and open returns
once the header fields arrive, with the body to read as a stream. To
send requests on connections kept open between them, use a Session
(client.session()).
For https:// the client offers HTTP/2 and HTTP/1.1 through ALPN and
speaks the one the server chooses; the request and response are the same
either way. Over HTTP/2 the response’s reason is the standard phrase
for its status. Plain http:// uses HTTP/1.1.
Redirects (301, 302, 303, 307 and 308 with a Location) are followed,
up to max_redirects of them, as RFC 9110 describes: 303, and 301 or
302 after a POST, become a GET without the body; 307 and 308 repeat
the method and body. A request whose body comes from
Request.set_reader cannot be repeated, so its 307 or 308 is returned
as it is. When a redirect leaves the origin (scheme, host and port),
Authorization, Proxy-Authorization and Cookie are dropped. Make a
client with max_redirects 0 to get every redirect back as it is.
A client is [Shared]: make one and use it from every thread and task.
In a task, a request parks only the task while it waits.
Example
import http;
main(): int {
var client = http.Client();
var response = client.get("https://example.com/").value();
print(to_string(response.status)); # 200
print(response.text());
return 0;
}
Fields
max_redirects
const max_redirects: int
The most redirects one request follows; 0 follows none.
Constructors
Client()
construct(max_redirects: int = 10)
Creates a client that trusts the system’s root certificates for
https:// URLs. Where TLS is not available (Linux or macOS without
OpenSSL 3), http:// URLs still work and https:// ones fail.
Parameters
max_redirects— the most redirects one request follows; 0 returns every redirect as it is
Client()
construct(connector: tls.TlsConnector, max_redirects: int = 10)
Creates a client whose https:// connections use connector, for
instance one that trusts a private certificate authority. The client
sets the connector’s ALPN protocols to h2 and http/1.1.
Parameters
connector— the TLS connector to openhttps://connections withmax_redirects— the most redirects one request follows; 0 returns every redirect as it is
Methods
get
get(url: String): io.IoResult<Response>
Sends a GET request for url.
Parameters
url— an absolutehttp://orhttps://URL
Returns
- the response; the errors of
send
post
post(url: String, content_type: String, body: String): io.IoResult<Response>
Sends a POST request for url with body as a
Content-Type of content_type.
Parameters
url— an absolutehttp://orhttps://URLcontent_type— the body’s media type, such asapplication/jsonbody— the body
Returns
- the response; the errors of
send
send
send(request: Request): io.IoResult<Response>
Sends request and reads the response, body and all.
The client adds Host, Content-Length (when there is a body, or
for POST, PUT and PATCH; Transfer-Encoding: chunked for a body
from Request.set_reader of unknown length) and Connection: close,
replacing any the request has.
Parameters
request— the request; itsurlmust be absolute
Returns
- the response after any redirects, whatever its status; an error with
is_invalid_input()for a URL that is nothttp://orhttps://, the errors ofio.TcpStream.connect_hostandtls.TlsConnector.connect, or an error when the response is not valid HTTP/1.x, its body is larger thanMAX_RESPONSE_BODY, or there are more thanmax_redirectsredirects
open
open(request: Request): io.IoResult<Response>
Sends request and returns the response as soon as its header fields
arrive, with the body still to read: response.reader() returns it
as a BodyReader, which reads it from the connection as it arrives,
with no limit on its size. Use it for a large download, writing each
chunk to a file or through a compress.DecompressReader.
The connection closes when the body has been read to its end, when
the BodyReader is closed, or when the last reference to it goes.
The request goes out, and redirects are followed, as for send.
Parameters
request— the request; itsurlmust be absolute
Returns
- the response with its body unread; the errors of
send, except that the body may be any size. Errors reading the body come from theBodyReader.
Example
var response = client.open(http.Request("GET", "https://example.com/large.tar.gz")).value();
var archive = compress.DecompressReader(response.reader(), compress.Format.Gzip);
var file = io.File.create(io.Path("large.tar")).value();
for (var chunk in archive.chunks(65536)) {
file.write_all(chunk.value());
}
session
session(): Session
Returns a new Session that sends requests with this client’s
settings and keeps their connections open for the requests after
them.
Returns
- the session, with no connections yet
Session
struct Session
A run of requests that share connections: each connection stays open
after its response (HTTP/1.1 keep-alive) and the next request to the same
scheme, host and port uses it again, saving the TCP and TLS handshakes.
Make one with client.session(); it sends requests and follows redirects
with that client’s settings.
A connection goes back to the session once its response body has been
read to its end (which send does), unless the server asked to close it.
Up to max_idle_per_host connections wait for each scheme, host and
port; one idle for longer than idle_timeout is closed instead of used.
A request that finds its reused connection closed by the server is sent
again on a new one, unless its body came from Request.set_reader.
Over HTTP/2 the session keeps one connection per origin, which every
request to that origin shares at once on a stream of its own, so tasks
started with spawn_local can send through one session concurrently.
A session is not [Shared]: use it from one thread or task. Its
connections close when the last reference to it (and to the
BodyReaders it returned) goes, or at close().
Example
var session = http.Client().session();
for (var page in 1..>3) {
var response = session.get($"https://example.com/items?page={page}").value();
print(response.text());
}
# One connection carried all three requests.
Fields
client
var client: Client
The client whose settings the session uses: its TLS connector and
max_redirects.
Properties
idle_timeout
var idle_timeout: os.Duration { get; set; }
How long a connection may wait unused before the session closes it rather than send on it: 90 seconds unless changed. Servers close idle connections too, often sooner.
max_idle_per_host
var max_idle_per_host: ulong { get; set; }
The most idle connections kept for one scheme, host and port: 4 unless changed. 0 keeps none, so every request uses a new connection.
idle_count
var idle_count: ulong { get; }
The number of idle connections the session holds, for every host.
Constructor
Session()
construct(client: Client)
Creates a session with no connections yet; client.session() does
the same.
Parameters
client— the client whose settings the session uses
Methods
get
get(url: String): io.IoResult<Response>
Sends a GET request for url, as Client.get does.
Parameters
url— an absolutehttp://orhttps://URL
Returns
- the response; the errors of
Client.send
post
post(url: String, content_type: String, body: String): io.IoResult<Response>
Sends a POST request for url with body as a Content-Type of
content_type, as Client.post does.
Parameters
url— an absolutehttp://orhttps://URLcontent_type— the body’s media type, such asapplication/jsonbody— the body
Returns
- the response; the errors of
Client.send
send
send(request: Request): io.IoResult<Response>
Sends request on an idle connection to its host if there is one,
and reads the response, body and all, as Client.send does. The
request keeps the connection open, so the client sends no
Connection: close.
Parameters
request— the request; itsurlmust be absolute
Returns
- the response; the errors of
Client.send
open
open(request: Request): io.IoResult<Response>
Sends request as send does and returns at the response’s header
fields, as Client.open does. The connection goes back to the
session when the body has been read to its end; closing the
BodyReader early closes the connection.
Parameters
request— the request; itsurlmust be absolute
Returns
- the response with its body unread; the errors of
Client.open
close
close()
Closes every idle connection. The session stays usable: later requests open new connections.
BodyReader
struct BodyReader
The body of a response from Client.open, read from the connection as it
arrives. It is an io.Reader: read it with read, chunks or lines,
or hand it to anything that takes a stream. It undoes the framing (a
Content-Length, chunks, or the rest of the connection) and ends where
the body ends, then closes the connection.
Properties
is_done
var is_done: bool { get; }
Reports whether the body has been read to its end, or the reader closed.
Constructor
BodyReader()
construct(reader: _HttpReader, mode: int, length: ulong, reusable: bool)
Makes a reader of a body framed by mode on reader’s connection.
Client.open makes it; there is no other use.
Parameters
reader— the connection’s reader, positioned at the bodymode— how the body is framedlength— the body’s length, for aContent-Lengthbodyreusable— whether the server keeps the connection open after the body
Methods
read
read(buffer: u8[], max: ulong): io.IoResult<ulong>
Waits until some of the body has arrived, then appends up to max
bytes of it to buffer.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to read
Returns
- the number of bytes appended, 0 at the end of the body; an error when the connection fails, closes before the body ends, or the chunked framing is not valid
close
close()
Stops reading the body and closes the connection. Reading then returns 0, as at the end of the body. Closing a reader already done does nothing.
Server
The HTTP server: Server accepts HTTP/1.1 and HTTP/2 connections on
every worker of async.run and answers each request with a handler
function.
Source: runtime/http/server.pep
MAX_REQUEST_BODY
const MAX_REQUEST_BODY: ulong = 16777216
The largest request body a Server reads: 16 MiB. A larger one gets
413 Content Too Large.
Server
[Shared] struct Server
An HTTP/1.1 and HTTP/2 server.
Server.bind listens on an address (Server.bind_tls serves HTTPS);
serve then accepts connections on every worker of the running
async.run and serves each connection in a task of its own, calling the
handler for every request. Connections are kept open between requests
unless the client asks to close (or speaks HTTP/1.0).
The handler gets the Request with its body read in full and returns the
Response; the server adds Content-Length (and Connection: close
when it will close). A request the server cannot parse gets 400 Bad Request, one with header fields over 64 KiB 431 Request Header Fields Too Large, and one with a body over MAX_REQUEST_BODY 413 Content Too Large; the connection then closes.
HTTP/2 is served to a client that chooses it through ALPN (see
bind_tls) or that starts a plain TCP connection with the HTTP/2
preface (prior knowledge): each stream is a request, answered by the same
handler in a task of its own.
Example
import async;
import http;
import io;
hello(request: http.Request): http.Response {
var response = http.Response(200).set_text("hello from " + request.path());
response.headers["Content-Type"] = "text/plain";
return response;
}
main(): int {
async.run(() => {
var server = http.Server.bind(io.SocketAddress.parse("0.0.0.0:8080").value()).value();
server.serve(hello);
});
return 0;
}
Constructor
Server()
construct(listener: io.SharedTcpListener, secure: bool, acceptor: tls.TlsAcceptor)
Wraps a listening socket. Use Server.bind or Server.bind_tls.
Parameters
listener— the listening socketsecure— whether to serve HTTPS withacceptoracceptor— the certificate for HTTPS; ignored for HTTP
Static methods
bind
static bind(address: io.SocketAddress): io.IoResult<Server>
Listens for HTTP connections on address.
Parameters
address— the local address (port 0 for any free port)
Returns
- the server; the errors of
io.TcpListener.bind
bind_tls
static bind_tls(address: io.SocketAddress, acceptor: tls.TlsAcceptor): io.IoResult<Server>
Listens for HTTPS connections on address, presenting acceptor’s
certificate. It sets the acceptor’s ALPN protocols to h2 and
http/1.1, so clients that offer HTTP/2 get it.
Parameters
address— the local address (port 0 for any free port)acceptor— the certificate and key to serve with
Returns
- the server; the errors of
io.TcpListener.bind
Methods
local_address
local_address(): io.IoResult<io.SocketAddress>
Returns the address the server listens on, with the port the system chose for port 0.
Returns
- the local address, or a platform error
serve
serve(handler: shared (Request) => Response)
Serves connections until the task is cancelled (when async.run
ends): accepts on every worker and serves each connection in a task
on the worker that accepted it.
Parameters
handler— answers one request; it runs in the connection’s task, so it may wait (on sockets, files, locks) without holding up other connections
Panics
- outside
async.run
reason_phrase
reason_phrase(status: int): String
Returns the standard reason phrase for an HTTP status code.
Parameters
status— the status code
Returns
- the phrase, such as
Not Foundfor 404; an empty string for a code without one
The tls module
Encrypted connections: TLS 1.2 and 1.3 over TCP, for clients and servers.
Bring it in with import tls. A TlsConnector opens client connections
and checks each server’s certificate; a TlsAcceptor accepts connections
with a certificate and key from PEM files; a TlsStream reads and writes
like an io.TcpStream. In a task, a call that has to wait parks only the
task.
On Linux and macOS the module uses OpenSSL 3, loaded when the first connector or acceptor is made; on Windows it uses SChannel.
- Streams:
TlsConnector,TlsAcceptor, andTlsStream
Example
import tls;
main(): int {
var connector = tls.TlsConnector.create().value();
var stream = connector.connect_host("localhost", 8443).value();
stream.write_all("ping");
var reply = Array<u8>();
while (stream.read(reply, 4096).value() > 0) {
}
return 0;
}
Source: runtime/tls/module.pep
Connectors and streams
TLS connections: TlsConnector opens client connections, TlsAcceptor
accepts server connections, and TlsStream is one encrypted connection
over a TcpStream.
Source: runtime/tls/stream.pep
TlsConnector
[Shared] struct TlsConnector
Opens TLS client connections, checking each server’s certificate.
A connector trusts the system’s root certificates; add_root_certificate
trusts more, such as a test certificate authority. Each connect runs
the TLS handshake over a connected TcpStream and checks that the
server’s certificate is valid and issued for the host name given. Only
TLS 1.2 and 1.3 are offered.
A server that asks for a client certificate (mutual TLS) gets the one
set with set_identity; without one, the connector presents none.
A connector is [Shared]: make one and use it from every thread and
task. Configure it (add_root_certificate, set_identity) before the
first connect.
See also
TlsStream
Example
import tls;
var connector = tls.TlsConnector.create().value();
var stream = connector.connect_host("localhost", 8443).value();
stream.write_all("ping");
Constructor
TlsConnector()
construct(context: __ptr<void>)
Wraps a platform context. Use TlsConnector.create.
Parameters
context— the platform’s client context, or null for none
Static methods
create
static create(): io.IoResult<TlsConnector>
Creates a connector that trusts the system’s root certificates.
Returns
- the connector; an error when no TLS library is available (on Linux and macOS, OpenSSL 3:
libssl.so.3, or Homebrew’s or MacPorts’libssl.3.dylib)
Methods
add_root_certificate
add_root_certificate(path: io.Path): io.IoResult<void>
Also trusts the certificates in the PEM file at path, for servers
whose certificates the system does not trust, such as a private or
test certificate authority.
Parameters
path— a file of one or more PEM certificates
Returns
- an Ok result; an error when the file cannot be read or holds no certificate
set_identity
set_identity(certificate_chain: io.Path, private_key: io.Path): io.IoResult<void>
Presents the certificate chain in certificate_chain, with the
private key in private_key, to servers that ask for a client
certificate (mutual TLS, as a TlsAcceptor does after
require_client_certificate). A later call replaces the identity.
Parameters
certificate_chain— a PEM file: the client’s certificate first, then any intermediate certificatesprivate_key— a PEM file with the certificate’s private key (on Windows, PKCS #8:BEGIN PRIVATE KEY)
Returns
- an Ok result; an error when a file cannot be read or the key does not match the certificate
Example
var connector = tls.TlsConnector.create().value();
connector.set_identity(io.Path("client.pem"), io.Path("client-key.pem")).value();
var stream = connector.connect_host("internal.example.com", 443).value();
set_protocols
set_protocols(protocols: String[]): io.IoResult<void>
Offers protocols to every server through ALPN (RFC 7301), most
preferred first, such as ["h2", "http/1.1"]. After connect,
TlsStream.protocol names the one the server chose, if it chose one.
Call it before the first connect; a later call replaces the list.
Parameters
protocols— the protocol names, each 1 to 255 bytes
Returns
- an Ok result; an error when the list is empty or a name is empty or longer than 255 bytes
Example
var connector = tls.TlsConnector.create().value();
connector.set_protocols(["h2", "http/1.1"]).value();
var stream = connector.connect_host("example.com", 443).value();
var http2 = stream.protocol() == Optional<String>.Value("h2");
connect
connect(stream: io.TcpStream, host: String): io.IoResult<TlsStream>
Runs the TLS handshake as the client over stream and checks the
server’s certificate.
Parameters
stream— a connected TCP stream; theTlsStreamkeeps it, and nothing else should read or write it afterwardshost— the name the certificate must be for, also sent to the server as the requested name (SNI); an IP address is checked against the certificate’s addresses instead
Returns
- the encrypted stream; an error when the handshake fails or the certificate is not trusted (“certificate verify failed: …”), or an io error from the socket
connect_host
connect_host(host: String, port: u16): io.IoResult<TlsStream>
Connects to host on port with TcpStream.connect_host, then runs
the handshake as connect does.
Parameters
host— a host name or an IP addressport— the port, usually 443
Returns
- the encrypted stream; the errors of
TcpStream.connect_hostand ofconnect
TlsAcceptor
[Shared] struct TlsAcceptor
Accepts TLS server connections with one certificate.
An acceptor presents a certificate chain and its private key, read from
PEM files. Each accept runs the TLS handshake over a TcpStream that a
TcpListener accepted. Clients are not asked for certificates unless
require_client_certificate is called (mutual TLS).
An acceptor is [Shared], so the accept loops of every worker can share
one.
See also
TlsStream
Example
import io;
import tls;
var acceptor = tls.TlsAcceptor.create(io.Path("cert.pem"), io.Path("key.pem")).value();
var listener = io.TcpListener.bind(io.SocketAddress.parse("0.0.0.0:8443").value()).value();
while (true) {
var (tcp, peer) = listener.accept().value();
var accepted = acceptor.accept(tcp);
if (accepted.is_ok()) {
accepted.value().write_all("hello over TLS");
}
}
Constructor
TlsAcceptor()
construct(context: __ptr<void>)
Wraps a platform context. Use TlsAcceptor.create.
Parameters
context— the platform’s server context, or null for none
Static methods
create
static create(certificate_chain: io.Path, private_key: io.Path): io.IoResult<TlsAcceptor>
Creates an acceptor that presents the certificate chain in
certificate_chain with the private key in private_key.
Parameters
certificate_chain— a PEM file: the server’s certificate first, then any intermediate certificatesprivate_key— a PEM file with the certificate’s private key (on Windows, PKCS #8:BEGIN PRIVATE KEY)
Returns
- the acceptor; an error when a file cannot be read, the key does not match the certificate, or no TLS library is available (see
TlsConnector.create)
Methods
require_client_certificate
require_client_certificate(roots: io.Path): io.IoResult<void>
Asks every client for a certificate and accepts only clients whose
certificate is issued by one of the certificates in the PEM file at
roots (directly or through intermediates the client sends) and is
valid for client authentication. A handshake with a client that sends
no certificate, or an untrusted one, fails. Call it before the first
accept.
Parameters
roots— a file of one or more PEM certificates: the authorities that issue client certificates
Returns
- an Ok result; an error when the file cannot be read or holds no certificate
Example
var acceptor = tls.TlsAcceptor.create(io.Path("cert.pem"), io.Path("key.pem")).value();
acceptor.require_client_certificate(io.Path("clients-ca.pem")).value();
set_protocols
set_protocols(protocols: String[]): io.IoResult<void>
Accepts protocols through ALPN (RFC 7301), most preferred first:
the handshake chooses the first of them that the client offers, and
TlsStream.protocol names it. A client that offers no protocols
connects with none chosen; one that offers only protocols not in the
list fails the handshake with the no_application_protocol alert
(RFC 7301). Call it before the first accept.
Parameters
protocols— the protocol names, each 1 to 255 bytes
Returns
- an Ok result; an error when the list is empty or a name is empty or longer than 255 bytes
accept
accept(stream: io.TcpStream): io.IoResult<TlsStream>
Runs the TLS handshake as the server over stream.
Parameters
stream— a connection aTcpListeneraccepted; theTlsStreamkeeps it, and nothing else should read or write it afterwards
Returns
- the encrypted stream; an error when the handshake fails (including, after
require_client_certificate, a client with no trusted certificate), or an io error from the socket
TlsStream
[NotCopyable("owns a TLS connection and cannot be copied")] struct TlsStream
An encrypted connection: TLS over a TcpStream.
Get one from TlsConnector.connect or TlsAcceptor.accept. Reading and
writing work as on a TcpStream, on the decrypted bytes: read appends
what has arrived and returns 0 once the peer has closed the connection,
write may send part of the data, and read_exact and write_all loop
until done. In a task, a call that has to wait parks only the task.
shutdown() tells the peer that nothing more is coming (a TLS
close_notify). A peer that closes the TCP connection without one also
ends the stream: read returns 0.
Closing: the destructor closes the connection when the last reference
goes; close() closes it earlier. Neither sends close_notify; call
shutdown() first for that.
See also
TlsConnectorTlsAcceptor
Example
var stream = connector.connect_host("localhost", 8443).value();
stream.write_all("ping");
var reply = Array<u8>();
while (stream.read(reply, 4096).value() > 0) {
}
Constructor
TlsStream()
construct(tcp: io.TcpStream, session: __ptr<void>)
Wraps a finished handshake. Use TlsConnector.connect or
TlsAcceptor.accept.
Parameters
tcp— the TCP connectionsession— the platform’s session, or null for a closed stream
Methods
is_open
is_open(): bool
Returns whether the stream is open.
Returns
- false after
close()
protocol
protocol(): String?
Returns the protocol the handshake chose through ALPN, from the
lists TlsConnector.set_protocols and TlsAcceptor.set_protocols
set.
Returns
- the protocol name, such as
h2; none when no protocol was chosen or the stream is closed
read
read(buffer: u8[], max: ulong): io.IoResult<ulong>
Waits until data arrives, then appends up to max decrypted bytes of
it to buffer.
Parameters
buffer— the array the bytes are appended tomax— the most bytes to read; 0 reads nothing and returns 0
Returns
- the number of bytes appended, 0 once the peer has closed the connection; an error when the stream is closed, a TLS error, or an io error from the socket
read_exact
read_exact(buffer: u8[], count: ulong): io.IoResult<void>
Reads until exactly count more bytes are appended to buffer.
Parameters
buffer— the array the bytes are appended tocount— the number of bytes to read
Returns
- an Ok result once
countbytes are appended; an error when the connection ends first, or an error as forread. The bytes read before an error stay inbuffer.
write
write(data: u8[]): io.IoResult<ulong>
Encrypts and sends bytes from data. The send may be partial; use
write_all to send everything.
Parameters
data— the bytes to send
Returns
- the number of bytes sent; an error when the stream is closed, a TLS error, or an io error from the socket
write_all
write_all(data: u8[]): io.IoResult<void>
Sends all of data, repeating the send until every byte is sent.
Parameters
data— the bytes to send
Returns
- an Ok result once every byte is sent; an error as for
write
write_all
write_all(text: String): io.IoResult<void>
Sends all of the UTF-8 bytes of text, as write_all(u8[]) does.
No newline or terminator is added.
Parameters
text— the text to send
Returns
- an Ok result once every byte is sent; an error as for
write
Example
stream.write_all("ping");
write_string
[Deprecated("use 'write_all' instead", "write_all")] write_string(text: String): io.IoResult<void>
Deprecated: use write_all instead. Sends all of the UTF-8 bytes of
text, exactly like write_all(text).
Parameters
text— the text to send
Returns
- an Ok result once every byte is sent; an error as for
write_all
See also
write_all
shutdown
shutdown(): io.IoResult<void>
Tells the peer that nothing more is coming (sends a TLS close_notify). The peer’s reads return 0 after the data sent before.
Returns
- an Ok result on success; an error when the stream is closed, or an io error from the socket
close
close(): io.IoResult<void>
Closes the connection now, without a close_notify (see shutdown).
Closing a closed stream does nothing.
Returns
- an Ok result on success or when already closed; the platform error when closing the socket fails
The testing module
The test program pep-build --test builds: a main that lists the
entry file’s [Test] functions or runs one of them.
A test build loads testing into every file, as --import testing
would, and adds to the entry file a main (marked [TestMain], which
replaces the file’s own) that registers each [Test] function with a
TestRunner and calls run. Test files do not import it themselves:
the checks they call, such as expect_equal, are in the prelude, and
the fixture TempFolder (a scratch folder removed when released) is
here.
pep-test (pepper test) drives the program: it lists the tests with
--list and runs each in a child process of its own with --run NAME
(see the testing chapter).
Source: runtime/testing/module.pep
TestRunner
The main of a test program: TestRunner holds the tests of the files
it was built from and answers pep-test’s --list and --run NAME.
Source: runtime/testing/runner.pep
keep_for_the_program
keep_for_the_program(body: () => void)
Runs body and lets what it leaves allocated stay without failing the
test’s leak check: for what a program makes once, on first use, and keeps
until it ends, such as a lazily made global (async.Lazy), when a test is
the first to use it.
Call it at the start of the test, with the first use alone, so the rest of the test is still checked.
Parameters
body— the code that makes what the program keeps
Example
import tls;
[Test]
connects() {
# The first connector loads the TLS library, which stays loaded.
keep_for_the_program(() => {
tls.TlsConnector.create();
});
# ... the checks, under the leak check as usual
}
TestMain
[Attribute] struct TestMain
Marks the main that pep-build --test generates: in a test build it
replaces the entry file’s own main.
TestRunner
struct TestRunner
The tests of one test program, which its generated main registers and
then runs with the program’s arguments.
The program answers two requests, both made by pep-test:
--listprints one line per test, its fields separated by tabs: the name,file:line,returnsorpanics(for[ShouldPanic]), and the text the panic must contain (empty for any panic or none).--run NAME [--timeout SECONDS]runs the testNAME(orfile::NAME, which tells apart tests of one name in two files) and exits with 0 when it returns, or with 1 when it left an allocation behind (or released one too many); a panic aborts the program. With a timeout above 0, a test still running after that many seconds ends the program with exit status 124 and a message on standard error. However the program ends (the test returns, panics, crashes or times out), every process the test started ends with it (a job object on Windows, a process group elsewhere), so none holdspep-test’s pipes to its output open.
Constructor
TestRunner()
construct(file: String)
Creates a runner for the tests of file.
Parameters
file— the entry file’s path, aspep-buildwas given it, or the folder of the files of a test program built from several
Methods
from_file
from_file(file: String)
Makes file the file of the tests registered after this call.
Parameters
file— the path of the file that declares them, aspep-buildwas given it
add
add(name: String, body: () => void, line: int)
Registers a test that passes when body returns.
Parameters
name— the test function’s namebody— the test functionline— the line that declares it
add_should_panic
add_should_panic(name: String, body: () => void, line: int, message: String)
Registers a [ShouldPanic] test, which passes only when body
panics with a message containing message.
Parameters
name— the test function’s namebody— the test functionline— the line that declares itmessage— text the panic’s message must contain; empty for any panic
run
run(args: String[]): int
Answers the request args makes (see the struct’s description).
Parameters
args— the program’s arguments, the program first
Returns
- the program’s exit status: 0 when the request succeeded, 2 for a request it does not understand or a test it does not have
TempFolder
TempFolder, a test fixture: a fresh folder that is removed with
everything in it when the fixture is released.
Source: runtime/testing/temp_folder.pep
TempFolder
struct TempFolder
A fresh, empty folder under the system’s temporary folder, for a test that writes files. The folder and everything in it are removed when the last reference to the fixture is released, so a test that holds it in a local cleans up when it returns.
Its name joins the name the test gives with the process id, so tests running at once, each in a process of its own, never share a folder. When a folder of that name is left from an earlier run (whose process had the same id) and cannot be removed, a number is added to the name, so the folder is always new and empty.
Example
[Test]
writes_a_file() {
var folder = TempFolder("writes_a_file");
var path = folder.path.join("notes.txt");
io.File.write_text(path, "hello");
expect_equal(io.File.read_to_string(path).value(), "hello");
}
Fields
path
const path: io.Path
The folder’s path.
Constructor
TempFolder()
construct(name: String)
Makes the folder, removing what a crashed run left at its path, or taking a numbered name when that cannot be removed.
Parameters
name— a name for the folder, such as the test’s
Panics
- if the folder cannot be made
Building Pepper from source
Pepper is written in Pepper. The compiler, pep-build, is the Pepper
program in tools/compiler (also called pepc), and every other tool is a
Pepper program under tools/. Building them takes a Pepper compiler, so
the first one comes from a published toolchain, the build of main (stage
0), the same way CI and the release build get theirs. The Rust compiler
that once bootstrapped Pepper plays no part.
You need this to work on Pepper itself, or on a platform with no prebuilt binaries. To use Pepper, install it instead.
Setting up
Four steps, on every platform:
-
The build of main. Every merge into main that changes the compiler, the runtime or the tools publishes it as a prerelease named
x.y.z-dev, whichpepper install list --availablelists. Install it and make it the default, with the install script on a new machine or withpepper install:sh scripts/install.sh --version 0.4.0-dev # the -dev version listed pepper install add 0.4.0-dev && pepper install use 0.4.0-devA prerelease is picked only by its exact name, never by
updateorlatest.pepper install add --force <version>refreshes it. -
Platform prerequisites, for linking:
- Windows: Visual Studio 2022 or newer with the C++ workload (or the
standalone Build Tools), which
clanglinks through; PowerShell (pwsh7.x is what the setup script is tested with; Windows PowerShell 5.1 also works); and 7-Zip, only for-DevTools, which extracts lldb from the llvm.org installer rather than running it. - macOS: the Xcode Command Line Tools (
xcode-select --install), for the SDK, system headers andld. Theclangthat drives the link comes from the LLVM prefix when it has one, which it does on macOS. - Linux (Debian/Ubuntu names):
sudo apt install build-essential clang curl xz-utils.clangis the link driver, and on Linux nothing else provides one:llvm-20-devinstalls nobin/clanginto its prefix.
- Windows: Visual Studio 2022 or newer with the C++ workload (or the
standalone Build Tools), which
-
LLVM 20. The compiler links LLVM through the
pepper-llvmpackage, which binds LLVM 20, so building it needs an LLVM 20 development install. One command provisions it (every option is in Setting up LLVM):./scripts/setup-llvm.sh --devtools # macOS / Linux pwsh -File scripts/setup-llvm.ps1 -DevTools # Windows -
The private packages.
tools/compilerdepends onjustinboswell/pepper-llvmandtools/pep-lsponjustinboswell/pepper-lsp. The first build of each fetches its package with git into the tool’s.pep/deps, so your git needs read access to those repositories.
Where LLVM comes from
On macOS and Linux your package manager has an LLVM 20 and the script uses it:
| platform | where LLVM comes from | cost |
|---|---|---|
| Linux | apt install llvm-20-dev (plus libpolly-20-dev) | 4–8s, 400–750 MB |
| macOS | brew install llvm@20 | 13–17s |
| Windows | the official release archive | 896 MB download |
Windows needs the archive because nothing else on the platform works: the
llvm.org installer and the winget, scoop and chocolatey packages ship neither
llvm-config nor the component libraries. The script falls back to the
archive on Linux and macOS too, but only when the package manager has no
LLVM 20.
That fallback is sound on Linux but usually unusable on macOS: the
release tarballs are LTO builds, so their static libraries are LLVM bitcode,
and Apple’s linker rejects bitcode produced by a newer LLVM than its own
(could not parse bitcode object file … Unknown attribute kind).
Homebrew’s llvm@20 is not LTO-built and works, so on macOS install it with
brew and re-run the script. It warns about this when it takes the archive
path there.
The script records the prefix it settled on in .pep/llvm-prefix
(gitignored), which the test, release and CI scripts read (a git worktree
reads its main checkout’s). Re-run it on a new machine rather than copying
that file. With a valid install present it only rewrites the file, and
./scripts/setup-llvm.sh --check reports what it would use, changing
nothing.
Only the Windows archive path is large: budget roughly 5 GB of disk for
the tree plus its cached archive, and a few minutes for the first run.
Everything lands outside the repository, under %LOCALAPPDATA%\pepper or
~/.local/share/pepper, except a package-manager prefix, which stays
wherever apt or brew put it.
The debugger
--devtools sets up the lldb debugger, and is worth it: a compiler that
generates and links native code fails in native ways, an access violation
or SIGSEGV from inside LLVM or the code it generated, and lldb turns that
into a symbolized stack. On Windows it installs lldb, lldb-dap and
lldb-server into the LLVM prefix (an extra 335–434 MB), extracting them
from the llvm.org installer with 7-Zip and placing the embeddable Python
that liblldb.dll needs beside them. On macOS and Linux it checks for a
usable lldb and says how to install one if there is none.
PEPPER_SETUP_DEVTOOLS=1 turns it on for every run.
Build the compiler with -g for its own frames, then:
lldb --batch -o run -k "thread backtrace" -- target/bin/pep-build tests/hello.pep
On Windows the setup script leaves the PATH alone, so run
"$env:LOCALAPPDATA\pepper\llvm\20.1.8\bin\lldb.exe" by its full path.
Debugging covers debugging Pepper programs.
Building
Build the compiler from tools/compiler with the installed toolchain,
against this checkout’s runtime, then every tool with the compiler you
built. Each tool is a project: its tools/<tool>/pep.toml adds tools/ to
the import search path, for the modules the tools share, and names the
packages it depends on. On macOS and Windows the compiler’s link needs
LLVM’s library folder (-L), and on Windows its LLVM-C.dll beside it:
export PEPPER_RUNTIME="$PWD/runtime"
llvm="$(head -1 .pep/llvm-prefix)"
mkdir -p target/bin
pepper build -O tools/compiler/main.pep -o target/bin/pep-build # + -L "$llvm/lib" on macOS and Windows
# Windows: cp "$llvm/bin/LLVM-C.dll" target/bin/
for tool in pepper pep-fmt pep-test pep-package pep-profile pep-run pep-install pep-doc pep-lsp; do
target/bin/pep-build -O "tools/$tool/main.pep" -o "target/bin/$tool"
done
(.exe names on Windows.) The release build (scripts/release-build.sh)
does the same, then builds the compiler once more with itself, and ships
that one. A cold build of the compiler takes several minutes; the
build cache makes a rebuild after
an edit take seconds.
Testing
The tests are Pepper [Test] functions, run by scripts/pepper-tests.sh
as CI runs them; there is no cargo test.
scripts/pepper-tests.sh # with the installed build of main
scripts/pepper-tests.sh target/bin # with the compiler you built
PEPPER_OPT_LEVEL=2 scripts/pepper-tests.sh # every build the tests make at -O2
WASI_SDK_PATH=/opt/wasi-sdk WASMTIME=/opt/wasmtime/wasmtime scripts/pepper-tests.sh
The last also builds the docs examples and the corpus for wasm32-wasip1
and runs them under wasmtime, except those that
need threads, tasks, sockets or processes; without a
wasi-sdk those tests say they
skipped and pass. Testing the runtime and the compiler says
what runs and where new tests go. Before a pull request, also run what CI
checks:
target/pepper-tests/bin/pep-doc check # the reference and the tools' pages are current
target/pepper-tests/bin/pep-fmt --check runtime tools
AGENTS.md, at the repository’s root, holds the rules every change
follows, for people and agents alike: the reference-counting model, how
errors are reported, naming, formatting, and which docs a change must
update. INTERNALS.md is the map of the compiler: where each stage lives
and the patterns it follows. .agents/skills/ holds step-by-step
workflows (build, test, finish a PR, test another platform), and its
build and test skills carry the failure signatures this project has
hit, such as LLVM discovery and link failures and the private packages’
fetch. Start there before diagnosing a failure from scratch.
The VS Code extension from a branch
The released extension comes from the vsix-v* GitHub release. To run the
extension and language server from the branch checked out, before it
merges:
scripts/install-vscode-extension.ps1 # build pep-lsp + vsix, install into VS Code
scripts/install-vscode-extension.ps1 -Watch # follow the branch: pull and reinstall on change
The extension restarts its language server by itself when the installed
server changes; a change to the extension’s own code needs Developer:
Reload Window. When editing the server, setting pepper.lspPath to a
pep-lsp you build skips the install (see
The language server: pep-lsp). Packaging
(vsce package, run by the script and the VSIX workflow) bundles
tools/vscode-pepper/src and its dependencies into one file with esbuild
(npm run bundle).
Platforms
- Windows x64, Linux x64 and macOS arm64 build and pass the suite; CI runs it on all three.
- Cross-compiling (
--target): CI links from each of Linux x64, macOS arm64 and Windows x64 for the other two withlldand their system roots and runs the programs on their own OS. Other pairs emit their objects; their links are untested. - WebAssembly (
--target wasm32-wasip1) and iOS and Android: see Platform selection and iOS and Android. - macOS x86_64 is untested. LLVM 20 publishes no
macOS-X64archive, so it needs Homebrew’sllvm@20.
To exercise another platform from the machine you are on, run the
platform-test workflow (gh workflow run platform-test.yml -f platforms=linux); the platform-test skill in .agents/skills/ describes
it.
Setting up LLVM: setup-llvm.sh
The compiler, pep-build (tools/compiler), links LLVM 20 through the
pepper-llvm package, so building it from source needs an LLVM 20
development install. The setup scripts provision one:
./scripts/setup-llvm.sh --devtools # Linux and macOS
pwsh -File scripts/setup-llvm.ps1 -DevTools # Windows
They record the LLVM they settled on in .pep/llvm-prefix (gitignored),
which scripts/pepper-tests.sh, the release scripts and the workflows read.
Run them again on each new machine rather than copying that file. Building Pepper from source says what each platform needs
first.
scripts/setup-llvm.sh [options]
pwsh -File scripts/setup-llvm.ps1 [-DevTools] [-Force]
What it does
- provisions LLVM 20, the one LLVM major Pepper builds with, cheapest source
first: an LLVM 20 that is already installed, else the platform’s package
manager (apt
llvm-20-devor Homebrewllvm@20), else the LLVM release archive (cached, so a second run is free). Windows always uses the archive: no Windows package shipsllvm-configand the component libraries; - writes the prefix, one line, to
.pep/llvm-prefixin the checkout; - does nothing more when a valid install is already there.
Options
scripts/setup-llvm.sh takes these; --help prints them.
| Option | What it does |
|---|---|
--devtools | Also check for the lldb debugger, with instructions when it is missing |
--no-devtools | Skip the debugger, even when PEPPER_SETUP_DEVTOOLS is set |
--no-packages | Do not use apt or Homebrew: use an LLVM already installed, or the release archive |
--check | Report the LLVM it would use and what .pep/llvm-prefix records, changing nothing; the status is 0 only when they agree |
-h, --help | Print the options |
Of --devtools and --no-devtools, the last one given wins. An unknown
argument is an error that exits with status 2.
scripts/setup-llvm.ps1 takes -DevTools, which installs lldb,
lldb-dap and lldb-server into the same prefix, and -Force, which
downloads and extracts the archive again even when a valid install is there.
Environment variables
| Variable | What it does |
|---|---|
PEPPER_SETUP_DEVTOOLS | 1 sets up the debugger on every run, as --devtools does, so it can be turned on once per machine |
Building this documentation
This book lives in docs/ and is rendered with mdBook.
The standard library reference (docs/src/reference/) is generated from the
runtime’s doc comments by pep-doc, a Pepper tool in this repository
(tools/pep-doc), which scripts/pepper-tests.sh builds beside pep-build.
Install mdBook once, from its releases
(a single binary to put on the PATH), then:
pep-doc serve # live preview while writing
pep-doc reference # regenerate the reference after changing runtime docs
pep-doc build # render into target/docs-site/dev/
pep-doc build --version 0.2.0 --site <dir>
Run it as target/pepper-tests/bin/pep-doc after scripts/pepper-tests.sh,
or as pepper doc.
pep-doc’s commands and options are described in
The documentation builder: pep-doc.
Keeping the book current
Documentation changes land in the same PR as the code they describe. The
checklist of what to update for each kind of change is in AGENTS.md, under
“Keep the documentation current”.
Examples are tested
Every runnable example lives in docs/examples/: a single name.pep, or a
folder name/ whose entry point is main.pep. Chapters pull them in with
mdBook’s include syntax, so the text on the page is the file that was tested:
```pepper
{{#include ../../examples/structs.pep}}
```
tools/pep-doc/examples_test.pep, which scripts/pepper-tests.sh runs,
builds and runs every example; each must return 0 from main and uses
assert to check what it shows. PEPPER_DOCS_EXAMPLE=structs limits it to
the examples whose name starts with structs. A sample of the examples is
also built at -O2 -g, where LLVM checks the debug info. Short fragments that are not whole programs can stay inline in the
chapter.
Versions
Documentation is versioned with the compiler’s semantic version
(the one line of the root VERSION file). pep-doc build --version X.Y.Z
renders the book into <site>/X.Y.Z/ and leaves every other version in the
site alone; versions.json lists them for the version picker, and the site’s
index.html opens the newest release. Builds from main publish as dev.
The Docs workflow (.github/workflows/docs.yml) publishes to the
gh-pages branch: pushing to main updates dev, and pushing a tag
vX.Y.Z adds the X.Y.Z docs. Published versions are never deleted.
The reference stays current
pep-doc check fails when docs/src/reference/ does not match the
runtime, and CI runs it.
What the compiler provides rather than the runtime declares is still
declared in the runtime, marked [__BuiltIn]: runtime/reflect.pep
declares the reflect functions of static blocks and the values they
return, and runtime/builtins.pep the raw pointer and weak reference
types, __null, sizeof, unsafe_cast and the __ intrinsics. The compiler ignores such a declaration (its functions and methods end in
;, with no body), so
it costs nothing and cannot clash with the built-in; the reference, the doc
comment check and the language server read it like any other. A string
argument names the built-in when it is not the declaration’s own name:
[__BuiltIn("reflect.name")] titles the entry and its signature
reflect.name, and hover and go-to-definition on a use of reflect.name
lead to it. Only the runtime may use the attribute.
Each reference page’s title and place in the table of contents come from
pages() in tools/pep-doc/reference.pep. A new runtime file with public
declarations must be added there, under its module’s module.pep; until it
is, reference and check fail and name the file.
check also fails on chapter Markdown that renders broken without an
mdBook error. An include indented inside a list item is one: mdBook pastes
the file’s lines at column 0, which ends the list and the code block, so the
rest of the example shows as prose and its # comments as headings. Put an
included example at column 0, after the list or in its own ### section. The runtime itself must meet the doc
comment standard in AGENTS.md, which pep-doc check enforces.
Tool options stay documented
Every command-line option of every tool has its place in the book:
docs/src/tools/<tool>.md for pep-build, pepper, pep-fmt, pep-lsp
and pep-doc, and Setting up LLVM for scripts/setup-llvm.sh.
pep-doc check takes each tool’s options from the tool itself (its
--help text, from the tools that scripts/pepper-tests.sh builds beside
pep-build) and fails, naming the tool, the option and the page, when a
page does not mention one. It also checks pep-build’s option values,
trace channels and environment variables, and pep-lsp’s initialization
options and VS Code settings. A new option goes in the tool’s --help (or
the usage text at the top of setup-llvm.sh) and on its page in the same PR.
Testing the runtime and the compiler
The tests are written in Pepper: [Test] functions (see
Unit tests) that pepper test
(pep-test) builds and runs. They cover the
runtime and the Pepper tools, the compiler written in Pepper
(tools/compiler) among them.
They are the whole suite: Pepper is written in Pepper, and so are its tests.
Where runtime tests live
| Code under test | Its tests |
|---|---|
A module, runtime/<module>/ | runtime/<module>/test/<topic>_test.pep, which imports the module |
A prelude file, runtime/<name>.pep | runtime/test/<topic>_test.pep |
| A file’s private functions | [Test] functions in that file itself |
A Pepper tool, tools/<tool>/ | tools/<tool>/<topic>_test.pep, which imports the tool’s files (import "format";), or runs the built tool (see Tests that run the tools) |
A test/ folder holds no module.pep, so no program that imports the
module loads it. Its files are still files of the module, though: a test file
that imports one of the module’s files (import "../atomic"; in runtime/async/test/) sees its
unexported names. pep-test builds each
folder’s tests into one program, so a helper (or test name) that two test
files of one folder declare collides; mark it private in each (as
runtime/async/test/ does). Prefer it to tests inside a
source file, which only a test of private internals needs. The reference
generator and the doc-comment check skip test/ folders, so a test file
needs no ## comments.
Running them
scripts/pepper-tests.sh runs them all:
sh scripts/install.sh --version 0.4.0-dev # the build of main; see below
scripts/pepper-tests.sh # every runtime and tool test
scripts/pepper-tests.sh -f parses # by name, in each run
scripts/pepper-tests.sh target/bin # with a pep-build you built
The tests build with the installed toolchain’s pep-build, the one
pepper install which names. Every merge into main that changes the
compiler, the runtime or the tools publishes the build of main as a
prerelease named x.y.z-dev (pepper install list --available shows it),
replacing the one before; CI and the workers install it with the install scripts
rather than building the compiler, and so can you (run the install again to
refresh it). The compiler’s own tests (tools/compiler/*_test.pep) build
the compiler’s sources into their test programs, so they test a change to
it. To run the whole suite with the changed compiler too, build it from
tools/compiler (see
Building Pepper from source)
and pass its folder as the first argument.
The script builds the Pepper tools (pepper, pep-test, pep-doc and the
others) from the checkout with that pep-build, against the checkout’s
runtime, into target/pepper-tests/bin beside a link to the pep-build,
and runs pepper test:
- in
runtime/, every runtime test; - in
runtime/, thecompresstests again with-D USE_ZLIBand thedigesttests with-D USE_LIBCRYPTO, which link the system’s zlib and libcrypto (on Windows the zlib run needsPEPPER_TEST_ZLIB_DIR, a folder holdingzlib.lib, and is skipped without it); - in
tools/, the tools’ tests, each under its tool’spep.toml.
The tools’ run, the longest, starts as soon as the tools its tests run
(pepper, pep-test, pep-fmt, pep-profile, pep-package and
pep-run) are
built, and runs beside the runtime’s runs; its output is printed after
theirs.
Once pepper is built, run any part of them by hand:
cd runtime
pepper test # every runtime test
pepper test json/test # one module's
pepper test -f parses test # by name
Tests run with runtime/ as the current folder, so a test reads data
files kept beside it by a path from there (archive/test/gnu.tar.gz).
The builds go to runtime/.pep/, which git ignores. A test that writes
files uses a fresh folder under the system’s temporary folder
(TempFolder in the testing module makes and removes one).
CI runs scripts/pepper-tests.sh with the installed build of main on
Linux, macOS and Windows. A PR that changes the compiler (tools/compiler/)
is tested with that compiler instead: CI builds it with the build of main
first (scripts/ci-build-compiler.sh) and runs the suite with it.
Two Pepper tools depend on private repositories. The compiler written in
Pepper, tools/compiler, depends on justinboswell/pepper-llvm, and the
language server tools/pep-lsp depends on justinboswell/pepper-lsp. CI
clones each over SSH with a read-only deploy key of that repository, whose
private half is this repository’s secret PEPPER_LLVM_DEPLOY_KEY or
PEPPER_LSP_DEPLOY_KEY.
scripts/ci-package-auth.sh writes the keys to temporary files. It gives
each package a host alias in an SSH config that offers only that package’s
key, since GitHub takes the first key offered and a deploy key opens only
its own repository. It points git at the aliases through GIT_CONFIG_* and
GIT_SSH_COMMAND variables of the test step only. Without a secret (in a
fork, say) the step says so in a notice, and the tests of the tool that
uses that package fail when they fetch it.
Tests that run the tools
Some tests run programs rather than call functions: pep-build on
programs, and the Pepper tools on files and folders, checking their
output and exit status as a user would see them.
| Test | What it runs |
|---|---|
tools/compiler/corpus_test.pep | pep-build -I tests on each tests/<Name>/test.pep, then the program from the repository’s root, which must exit with 0. With WASI_SDK_PATH set and wasmtime on the PATH (or WASMTIME naming it), each again for wasm32-wasip1 under wasmtime; it says it skipped otherwise. A new tests/<Name>/test.pep joins the run. |
tools/compiler/programs_test.pep | pep-build on the programs of tools/compiler/testdata/, each run with the arguments and environment it needs: cli’s parse and an add_options error, os’s environment and host, time’s zones (TZif files under TZDIR, TZ, the system’s database), and process’s arguments, exit, main’s arguments, children that are copies of the program, and exec. |
tools/pep-doc/examples_test.pep | pep-build on every example of the book, then the example (see Building this documentation). |
tools/pep-fmt/repository_test.pep | format_source in the test itself on every .pep file under runtime/, tools/, tests/ and docs/examples/: each formats, formatting it again changes nothing, the syntax tree stays the same, and the files of runtime/ and tools/ are already formatted (pep-fmt -w fixes one that is not). |
tools/pep-fmt/cli_test.pep | pep-fmt on standard input and on files and folders (--check, -w), its syntax errors and usage errors. |
tools/pepper/pepper_test.pep | pepper, copied beside stand-in tools built from tools/pepper/testdata/; and every tool’s --version against the version in each tools/*/main.pep (and pep-lsp’s tools/pep-lsp/arguments.pep). |
tools/pep-test/pep_test_test.pep | pep-test on test files it writes, from tools/pep-test/testdata/ (named .pep.txt so that the suite’s own run skips them). |
tools/pep-package/package_test.pep | pep-package and pep-build on projects whose dependencies are bare git repositories in a test folder, and archives on a loopback server, including builds of one project side by side, which fetch once under .pep/fetch.lock; skipped without git. |
tools/pep-run/pep_run_test.pep | pep-run building and running the programs in tools/pep-run/testdata/ with a Pepper home of its own: arguments, exit status, working folder, the up-to-date rerun, compile errors and the options it refuses. |
tools/pep-profile/profile_test.pep | pep-profile on the recordings in tools/pep-profile/testdata/, and on a real program when samply or a working perf is installed (skipped otherwise). |
They find pep-build and the tools beside the test program or on the
PATH (process.find_companion), where scripts/pepper-tests.sh puts the
ones it builds from the checkout, so they test this checkout’s tools. To
run one by hand, put that folder first on the PATH (after a run of the
script, or with the tools built there as the script builds them):
export PATH="$PWD/target/pepper-tests/bin:$PATH"
cd tools && pepper test pep-fmt/cli_test.pep
The tool_testing module (tools/tool_testing/) holds what they share:
tool(name) finds a tool, run and run_with_input run a
process.Command and read its output with Windows’ line ends made
\n, build_program builds a program with pep-build, and
expect_exit, expect_contains, expect_starts_with and
expect_ends_with show the program’s output when they fail.
tool_testing.server’s serve_folder serves a folder over HTTP on
loopback, for a test that downloads (pep-install, pep-package).
Leaks fail a test
Each test counts the runtime’s live allocations before and after it runs, and fails when they differ:
error: test 'parses_components' left 2 allocations behind
So a test needs no census of its own: every object it made must be
released by the time it returns. What the program keeps for good, such as
a global made on first use (async.Lazy), is made inside
keep_for_the_program(() => { ... }) at the start of the test, which
leaves it out of the count.