Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 extern C 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 * 2 can 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, so io.Path parameters accept a String (see implicit conversions).
  • Free functions, methods, operators, constructors, extension methods and trait members can be overloaded.
  • A static block 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=VALUE on the command line defines a build option, which [BuildOption("NAME")] and [Target(BuildOption(NAME) >= 2)] select code by and static blocks 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, bool and 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)), or send(copy(x)) to keep x), a Select waits to receive from or send to several channels at once, and a Mutex holds any struct, with the compiler keeping each locked value apart from the rest of the program (see threads). An RwLock lets many threads read a thread-safe value at once, and Semaphore, Barrier and Latch coordinate groups of threads. A [ThreadLocal] module-scope var gives 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_add and 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

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.