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

The Whim Programming Language

Whim is a toy programming language. Do not use it in production.

Whim is a small language with strict runtime types. Its syntax will feel familiar if you know PHP or Hack, but Whim follows its own rules.

function greet(string $name): string {
  return 'Hello, ' . $name . '!';
}

write_line!(greet('Ada'));

Whim has reified generics, value-based arrays, classes, interfaces, enums, pattern matching, async tasks, and a large standard library. The whim command runs and formats source files, prints bytecode, and manages Git dependencies.

This book explains the language as it works now. Whim has no promise of backward compatibility. A later release may change or remove any rule in this book.

Start with Installation, then write Your First Program.

Installation

Whim supports macOS on x86-64 and Arm64. It also supports glibc-based Linux on x86-64, Arm64, and RISC-V 64. Run it through the whim command.

Shell installer

Install the latest release on macOS or Linux:

curl --proto '=https' --tlsv1.2 -sSf https://carthage.software/whim.sh | bash

Pass a version to install a specific release:

curl --proto '=https' --tlsv1.2 -sSf https://carthage.software/whim.sh | bash -s -- --version=0.1.0

The installer verifies build attestations when a compatible GitHub CLI is available.

Note: Release 0.1.0 has no attestation.

Manual installation

Download the archive for your system from GitHub Releases. Put the whim file in a directory on your PATH. Then check it:

whim --version

Docker

The image at ghcr.io/carthage-software/whim supports amd64, arm64, and RISC-V 64. Mount a project and pass its entry file:

docker run --rm -v "$PWD:/app" ghcr.io/carthage-software/whim:latest main.whim

Each release publishes latest, the full version, and the major-minor version.

Build from source

Install Rust 1.98 or later. From the repository root, run:

cargo build --locked --release

The build produces the whim executable at target/release/whim.

Source files

Whim source files use .whim. Compiled artifacts use .whia.

Continue with Your First Program.

Your First Program

Create hello.whim:

write_line!('Hello, Whim!');

Run it:

whim hello.whim

Whim runs statements at file scope. You do not need a main function.

Variables

A variable starts with $. Assignment creates it:

$language = 'Whim';
$year = 2026;

write_line!($language . ' ' . $year);

Whim infers the variable’s type from its value. A later assignment may change that type:

$value = 10;
$value = 'ten';

assert!($value is string);

Use == and != for equality. Conditions must produce bool:

$name = 'Ada';
if ($name != '') {
  write_line!('Hello, ' . $name);
}

if ($name) is an error. Whim does not turn strings, numbers, arrays, or null into booleans.

Functions

A function declares each parameter and its return type:

function square(int $number): int {
  return $number * $number;
}

assert!(square(9) == 81);

Whim checks the argument before the call and checks the result before it returns to the caller.

Imports

Use a fully qualified name or import a symbol:

use Whim\Str;

$parts = Str\split('one,two,three', ',');
assert!(length!($parts) == 3);

One use form imports classes, interfaces, enums, functions, constants, aliases, and newtypes.

Format the file

From a project with a whim.toml, format every project source:

whim fmt

You may instead name one file or directory:

whim fmt hello.whim
whim fmt src

Use --check when you want a nonzero exit status instead of an edit:

whim fmt --check

The next chapter builds a program with files, loops, and a dict.

A Small Program

This program counts the words in a file. It uses command-line arguments, file I/O, functions, loops, and a dict.

Save it as words.whim:

use Whim\Env;
use Whim\File;
use Whim\Str;

function count_words(string $text): dict<string, int> {
  $counts = dict[];
  $words = Str\split(Str\lowercase($text), ' ');

  foreach ($words as $word) {
    $word = Str\trim($word);
    if ($word == '') {
      continue;
    }

    if (!contains_key!($counts, $word)) {
      $counts[$word] = 0;
    }

    $counts[$word]++;
  }

  return $counts;
}

$arguments = Env\get_arguments();
if (length!($arguments) < 1) {
  write_error_line!('usage: whim words.whim <file>');
  exit!(2);
}

$text = File\read($arguments[0]);
$counts = count_words($text);

foreach ($counts as $word => $count) {
  write_line!($word . ': ' . $count);
}

Run it:

whim words.whim README.md

Env\get_arguments() contains the arguments after the source file. The input path is at index 0.

dict[] creates an empty dict. Reading a missing key throws, so the program uses contains_key! before it reads the count. The first word starts at zero, then ++ adds one.

Dicts keep insertion order. The last loop prints words in the order in which the program first found them.

This parser splits only on spaces. A full word parser would also handle tabs, newlines, and punctuation. This example keeps the parser short so it can focus on the language.

The whim Command

The whim program runs source, formats it, prints bytecode, and manages Git dependencies.

Use whim --help or whim COMMAND --help for the installed version’s exact options.

Run

The short and explicit forms are equal:

whim app.whim one two
whim run app.whim one two

The first argument after the file has index zero in Whim\Env\get_arguments().

Use - as the file to read source from standard input:

printf "write_line!('hello');\n" | whim -

Source read this way cannot use embed! because it has no directory.

Global options must come before the source file:

OptionEffect
`–colors autoalways
--config PATHload settings from another whim.toml

Use -- before a source path that starts with -.

whim run reads the nearest whim.toml, loads the bundled standard-library artifact, then compiles the entry file and files that the program loads. It does not inspect whim.lock or vendor/.

Runtime settings belong in [runtime]:

[runtime]
optimizations = "on"
call-depth = 10000
cycle-threshold = 10001
full-trace = false

Each field is optional. For one run, use WHIM_OPTIMIZATIONS, WHIM_CALL_DEPTH, WHIM_CYCLE_THRESHOLD, or WHIM_FULL_TRACE. Environment values override the file:

WHIM_OPTIMIZATIONS=off whim app.whim
WHIM_FULL_TRACE=true whim app.whim

Format

whim fmt
whim fmt src/ tests/
whim fmt --check

With no paths, the command finds the nearest whim.toml and formats the project from that file’s directory. It skips vendor/ and .git/. A command with no paths fails when it cannot find a manifest.

The command also accepts files and directories. An explicit directory ignores format.include but honors exclusions. An explicit file bypasses both. --check reports files that would change and writes nothing.

Project selection belongs in [format]:

[format]
include = ["**/*.whim"]
exclude = ["src/generated/**"]

Patterns use / and are relative to the manifest directory. Exclusion wins. The default inclusion is **/*.whim; vendor/ and .git/ are always excluded from directory walks.

Layout settings can come from the nearest whim.toml, a file passed through the global --config option, or command options:

  • --print-width N
  • --tab-width N or --tab-size N
  • --use-tabs true|false
  • --end-of-line lf|crlf

Command options override file settings.

Disassemble

whim disassemble app.whim
WHIM_OPTIMIZATIONS=off whim disassemble app.whim
printf "write_line!('hello');\n" | whim disassemble -

This command compiles the program and prints its register bytecode. It does not run the entry file. WHIM_OPTIMIZATIONS=off prints the form before optimization.

Language server

whim language-server

The language server speaks LSP over standard input and output. It provides keyword completion, snippets, formatting, keyword colors, folding, selection ranges, and occurrence highlights. It does not index the project or provide symbol navigation.

Project commands

init, add, remove, install, and update change dependency state. show, why, suggestions, and fund inspect it. why-not tests a new requirement against the graph. It creates and locks the project cache and may fetch Git tags, so it needs a writable project and network access.

These commands search the current directory and its parents for whim.toml, except init, which creates a project in the current directory. whim init --no-git skips Git setup.

Package command failures leave the old manifest, lock, vendor tree, and loader together. See Git Dependencies.

Log output

WHIM_LOG sets the CLI log filter. WHIM_LOG=off disables all log output, including error-level records. A CLI command can therefore fail with no error text while still returning a nonzero status. This is intended.

Log errors are not Whim exceptions. Disabling log output does not catch or change a thrown value, an uncaught throwable, or a panic! trace.

Exit status

The CLI returns zero on success. exit!($status) selects another status. panic! and an uncaught throwable use 255. CLI errors return a nonzero status. Error text goes to standard error; program output goes to the handle used by its write calls.

Source Text and Comments

A Whim source file contains UTF-8 text. The usual file suffix is .whim. ASCII spaces, tabs, line feeds, carriage returns, vertical tabs, and form feeds separate tokens but have no other meaning. The formatter uses two spaces for each level by default.

Identifiers

An identifier starts with an ASCII letter, _, or a non-ASCII UTF-8 byte. Later bytes may also be decimal digits. A backslash joins namespace segments.

$answer2 = 42;

function café(): string {
  return 'coffee';
}

assert!(café() == 'coffee');

Whim compares names by their UTF-8 bytes. It does not fold case or normalize Unicode. Two spellings that look alike may still name different symbols.

Statements and blocks

Most simple statements end with ;:

$answer = 42;
write_line!($answer);

A block uses braces and does not take a trailing semicolon:

if (true) {
  write_line!('inside the block');
}

Control-flow headers use parentheses. This applies to if, while, for, foreach, and catch.

Comments

Whim has line comments, block comments, and doc comments:

// This comment ends with the line.

/* This comment may
   span several lines. */

/** Returns the answer. */
function answer(): int {
  return 42;
}

A doc comment belongs to the declaration that follows it. # does not start a comment. The sequence #[ starts an attribute.

Shebang line

A file may start with a Unix shebang:

#!/usr/bin/env whim

The shebang must start at byte zero. Whim treats # anywhere else as an error unless it begins an attribute.

Number literals

Integers use signed 64-bit values. The source forms are:

assert!(42 == 4_2);
assert!(0xff == 255);
assert!(0b1010 == 10);
assert!(0o755 == 493);

Underscores may split digits. A decimal integer cannot start with 0 unless it is zero. Write 0o for octal.

Floats use decimal digits and may use an exponent. A decimal point may have digits on only one side:

assert!(1.5 == 15e-1);
assert!(.5 == 0.5);
assert!(5. == 5.0);
assert!(1_000.0 == 1000.0);

The runtime stores floats as IEEE 754 double-precision values.

String tokens

Strings explains single-quoted and double-quoted strings. Backticks do not quote strings.

Top-level declarations

Functions, classes, interfaces, enums, constants, aliases, and newtypes may appear only at file or namespace scope. They cannot appear inside a function or control-flow block.

File-scope statements form that file’s executable body. A loaded file may both declare symbols and run code.

Names and Keywords

Whim names are case-sensitive. User, user, and USER are three names.

Variables and symbols

A variable starts with $:

$value = 42;

Declared symbols do not:

const LIMIT = 10;

function limit(int $value): int {
  if ($value > LIMIT) {
    return LIMIT;
  }

  return $value;
}

Classes, interfaces, enums, functions, constants, aliases, and newtypes share one symbol table. Two such declarations cannot use the same qualified name.

Properties have their own member names. Methods, class constants, and enum cases share the class-like member table. They cannot reuse one name in the same family.

Qualified names

A backslash separates namespace parts:

App\Model\User

A leading backslash starts at the global namespace:

\Whim\Json\encode

Without that leading slash, Whim resolves a qualified name through imports and the current namespace.

The reserved _ name

The bare name _ cannot name any symbol or member. Whim uses it as a wildcard, a match default, and an unnamed generic slot.

Variables include $, so $_ is valid. It is an ordinary variable, not a discard:

$_ = 'kept';
assert!($_ == 'kept');

Keyword levels

Whim has three keyword levels.

Full keywords, such as if, match, and return, cannot name a function or a constant. Soft keywords, as and is, may name functions but not constants. Context keywords, such as class, int, and readonly, may name functions or constants where the parser can tell what they mean.

Every keyword may name a class-like member because ->, ?->, ::, or a member declaration makes that use clear:

final class Tokens {
  public const MATCH = 'match';

  public function match(): string {
    return self::MATCH;
  }
}

assert!(new Tokens()->match() == 'match');

The keyword appendix lists every word and its level.

Namespaces and Imports

A namespace prefixes each declaration in its body.

File namespace

The common form applies from its declaration to the end of the file:

namespace App\Model;

final class User {}

The class name is App\Model\User.

Braced namespace

A braced namespace limits the prefix to one block:

namespace App\Math {
  function double(int $value): int {
    return $value * 2;
  }
}

assert!(App\Math\double(21) == 42);

Declarations remain top-level even when a namespace block holds them.

Imports

use imports one symbol under its last name:

namespace App;

use Whim\DateTime\Date;
use Whim\Json;

$date = Date::from('2026-08-21');
$json = Json\encode(dict['date' => $date->toString()]);
assert!($json != '');

An alias chooses another local name:

use App\Model\User as ModelUser;

One declaration may import several names:

use Whim\HTTP\Message\Request, Whim\HTTP\Message\Response;
use Whim\HTTP\Message\{FieldMap, ProtocolVersion};

The braced form shares the prefix before {. Either form permits an alias on each item and an optional trailing comma in a braced list.

Whim uses one import form for every symbol kind. It has no use function or use const form.

Two imports cannot claim the same local name. A declaration also cannot reuse an imported local name.

Imports do not load code

use changes name lookup only. It does not read a file or run an autoloader. Whim loads code through require!, require_once!, or a registered autoloader. The next chapter covers each path.

Constants and Initializers

A namespace constant binds one name to one value:

const ANSWER = 42;
const LABEL = 'answer';

assert!(ANSWER == 42);

Namespace constants have no written type. Their value gives them a type. A class-like constant may state a type, as the class chapter shows.

Constant expressions

Constants, attribute arguments, parameter defaults, and property defaults use constant expressions. Such an expression may use:

  • scalar literals and existing constants;
  • embed! with a literal relative path;
  • unary and binary operators;
  • tuple, vec, and dict literals, including vec and dict spreads;
  • a closure with no use list and no $this use;
  • a named class construction;
  • function, static method, and method calls whose inputs are constant expressions.

Calls may run code, inspect state, and throw. “Constant expression” names the source forms allowed at the use site. It does not mean pure or compile-time work. The expression itself cannot read a local variable or $this directly.

final class Box {
  public function __construct(public int $value) {}

  public static function from(int $value): Box {
    return new Box($value);
  }
}

function add(int $left, int $right): int {
  return $left + $right;
}

const TOTAL = add(20, 22);
const BOX = Box::from(TOTAL);

assert!(BOX->value == 42);

Forms that do not qualify

A constant expression cannot use:

  • a variable or $this;
  • assignment, indexing, or a property read;
  • interpolation;
  • a short closure or a closure capture;
  • match, throw, a partial call, or a language construct other than embed!;
  • vec[$value; $size];
  • a class name held in an expression.

The compiler reports which form broke the rule.

When Whim evaluates values

Whim evaluates a namespace or class constant when it declares that symbol. It evaluates a static property default when it declares the class.

Whim evaluates an instance property default for each new object. Two objects do not share an object made by that default. It evaluates an omitted parameter default for each call.

An attribute argument runs when Whim creates the attribute value for its target.

Self-reference and load order

A constant may refer to a constant declared later in the same compiled unit. Whim resolves the name before it starts the unit. A constant cannot depend on itself, whether the path is direct or passes through other constants.

Code may use a loaded constant from another unit. An unknown constant may run the autoloader. A failed load leaves the constant undefined.

Loading Files

Whim can compile and run another source file while a program runs.

require!

require!($path) resolves the path, compiles the file, links its symbols, and runs its file-scope statements:

require!(directory!() . '/support.whim');

directory!() returns the directory of the current source file. Use it for a path that should not depend on the process working directory.

A source file returns null. A parse, compile, link, or file error throws. A symbol declared by a successful load becomes available to later code.

require! runs the file each time. A file that declares symbols cannot usually run twice because the second load would redeclare them.

require_once!

require_once!($path) loads a resolved path at most once:

require_once!(directory!() . '/vendor/autoload.whim');

A later call for the same resolved path returns null without running the file again. Circular require_once! calls stop when they reach a file whose load has started but not ended.

If require! loaded a path first, require_once! also treats it as loaded.

Whim compiles a loaded file as one unit. Declarations in that unit can refer to each other even when the source declares them later.

The linker rejects duplicate names before it publishes the new unit. A failed load does not replace an existing symbol.

Autoloading

When Whim needs an unknown symbol, it may ask registered autoloaders to define it. Type checks, generic arguments, property types, constants, and ordinary calls may all trigger autoloading.

An autoloader must return true only after it has defined the requested symbol. See Autoloading for the API and generated Git dependency loader.

Compilation and Execution

Whim compiles source to register bytecode, links its declarations, then runs its file body. The whim command performs these steps in one process.

Program start

For whim app.whim, the command:

  1. creates the language core;
  2. loads the bundled standard-library artifact;
  3. reads and compiles app.whim;
  4. links its declarations;
  5. runs its file-scope statements; and
  6. runs all remaining destructors.

The core contains the symbols that the language itself needs. The standard library is a precompiled .whia artifact made from the source under lib/src/. The entry file sees both sets of symbols.

The command reads runtime settings from the nearest whim.toml. It does not read whim.lock or vendor/. A project that uses Git packages must require its generated loader.

One source unit

The compiler treats one source file as one unit. It parses the whole unit before it runs any of it. Declarations in that unit may refer to declarations written later in the same file.

Whim reports failures by stage:

  • ParserError for invalid source tokens or syntax;
  • CompilerError for a rule that one unit breaks;
  • LinkerError for a conflict between declared symbols or class contracts.

The linker publishes a unit only after its declarations pass. A failed unit does not replace a symbol that already exists.

Initializers and the file body

After linking, Whim evaluates namespace constants, class constants, and static property defaults. It also checks the values of static properties. An initializer may call code or throw, so a unit can fail before its first file-scope statement.

Instance property defaults run when code creates an object. Parameter defaults run when a call omits that argument. Attribute arguments run when code asks Whim to create the attribute object.

File-scope statements then run from top to bottom. Their locals belong only to that file body. The body returns null when it ends.

Optimization

The compiler optimizes each unit before it runs. It may remove a type check that it proves will pass, choose a bytecode instruction for a known value kind, fold a fixed expression, or inline an eligible call.

Optimization must not change program results. Use WHIM_OPTIMIZATIONS=off to compare a problem with plain bytecode. Use whim disassemble to inspect either form.

Loaded files

require!, require_once!, and autoloaders compile more units in the same process. Each successful unit adds its symbols to the current program. Its file-scope statements run under the same event loop and use the same heap, standard handles, environment, and process state.

A loaded unit cannot redeclare an existing symbol. require_once! identifies a file by its resolved path and stops a second run of that path.

Calls, tasks, and shutdown

Normal calls use one stack of call frames. Async tasks use separate stacks and cooperate on one event loop. A task runs until it returns, throws, or reaches a wait that suspends it.

The entry body does not drain the event loop on its own. Call Async\drain() or await a future to run scheduled work. Referenced tasks keep Async\drain() running; unreferenced tasks do not.

After the entry body ends, Whim runs remaining destructors. An uncaught throwable ends the program with status 255. exit! ends it with the requested status and does not run pending finally blocks. Shutdown destructors still run after exit!. panic! prints a message and trace, uses status 255, skips pending finally blocks, and runs shutdown destructors.

Built-in Values

Every Whim value has a runtime type. A declaration can state which values it accepts. Whim checks that rule when the program reaches the boundary.

Scalar values

Whim has five scalar value kinds.

TypeValues
nullnull
booltrue and false
intsigned 64-bit integers
floatdouble-precision floating-point numbers
stringbyte strings

Integers and floats stay distinct:

assert!(1 is int);
assert!(1.0 is float);
assert!(1 != 1.0);

Strings hold bytes, not a promise of valid UTF-8. The source file is UTF-8, but a string may contain any byte through escapes, file reads, sockets, or binary decoding.

Arrays

Whim has three array forms:

TypeMeaning
(A, B)an immutable, fixed-size tuple
vec<T>a mutable list with integer keys from zero
dict<K, V>a mutable, ordered key-value map

array<K, V> accepts a tuple, vec, or dict whose keys and values fit K and V.

$pair = ('Ada', 36);
$names = vec['Ada', 'Grace'];
$scores = dict['Ada' => 10, 'Grace' => 12];

assert!($pair is (string, int));
assert!($names is vec<string>);
assert!($scores is dict<string, int>);

The collections chapter covers their syntax and update rules.

Objects and callables

object accepts any class instance. A class or interface name accepts objects of that class or its subtypes.

fn(A, B): R accepts a callable with two parameters and result R:

function apply(fn(int): int $operation, int $value): int {
  return $operation($value);
}

$double = fn(int $value): int => $value * 2;
assert!(apply($double, 21) == 42);

classname<T> accepts a class name whose instances satisfy T.

Wide and empty types

mixed accepts every value. !never also means every value.

never accepts no value. A function with return type never cannot return. It must throw, exit, or keep running.

void is valid only as a function or method return. Such a callable returns no value. You cannot use void for a parameter or property.

Literal types

A scalar value may also act as a type:

function choose('yes'|'no' $answer): bool {
  return $answer == 'yes';
}

assert!(choose('yes'));

true, 42, and 'yes' each describe one value. Literal types make unions, ranges, enum cases, and constants precise.

Conditions

Conditions must be bool. Whim has no truthy or falsy conversion:

$name = 'Ada';
if ($name) {
  write_line!($name);
}

Write the test you mean:

$name = 'Ada';
if ($name != '') {
  write_line!($name);
}

This rule also applies to while, do ... while, &&, ||, &&=, and ||=.

Strings

A Whim string is a sequence of bytes. It may contain text, encoded data, or arbitrary binary data.

Single quotes

Single quotes do not interpolate variables. They recognize \\ and \'. Other backslash pairs stay as written:

$name = 'Ada';
assert!('Hello, $name' == 'Hello, $name');
assert!('it\'s' == "it's");
assert!('\n' == "\\n");

Use single quotes when the value needs no interpolation or control-byte escape.

Double quotes

Double quotes support escapes and interpolation:

$name = 'Ada';
$next = 41;

assert!("Hello, $name" == 'Hello, Ada');
assert!("answer: {$next + 1}" == 'answer: 42');

The short form accepts one variable. Braces accept any expression. An interpolated value must be a string, int, or float.

Escape $, {, or } when you need the byte itself:

assert!("\$name \{value\}" == '$name {value}');

Escape sequences

Double-quoted strings support these escapes:

EscapeByte or text
\\backslash
\"double quote
\$, \{, \}interpolation marker as text
\n, \r, \tline feed, carriage return, tab
\v, \f, \evertical tab, form feed, escape
\xH, \xHHone byte from one or two hex digits
\O, \OO, \OOOone byte from up to three octal digits
\u{H...}one Unicode scalar encoded as UTF-8

An octal escape must fit in one byte. A Unicode escape cannot name a surrogate or a value above 10FFFF.

An unknown escape keeps its backslash:

assert!("\x" == '\x');

Length and indexing

length! counts bytes. Indexing returns a one-byte string:

$text = 'abc';
assert!(length!($text) == 3);
assert!($text[1] == 'b');

An index must be an in-range integer. An invalid index throws OutOfBoundsError. Strings are immutable, so indexed assignment fails.

Concatenation

. joins strings, ints, and floats:

assert!('count=' . 3 == 'count=3');
assert!('value=' . 1.5 == 'value=1.5');

It does not convert bool, null, arrays, objects, or callables. Use an explicit conversion for those values.

Text and bytes

Whim\Str works on bytes. Its case functions handle ASCII. Use Whim\Encoding\UTF8 to check or repair UTF-8, and use Whim\Binary for fixed binary formats.

Tuples, Vecs, and Dicts

Whim arrays are values. Tuples are fixed and immutable. Vecs and dicts are mutable.

Tuples

A tuple may hold a different type at each position:

$user = (7, 'Ada', true);
assert!($user[0] == 7);
assert!($user is (int, string, bool));

A one-item tuple needs a trailing comma:

$one = ('only',);
assert!($one[0] == 'only');

() is not a value. Tuples may hold 1 through 12 items. Their indexes do not change, and tuple elements reject writes.

A tuple type may end with a rest type:

function read_row((int, ...string) $row): int {
  return $row[0];
}

assert!(read_row((7, 'a', 'b')) == 7);

Vecs

A vec has dense integer keys from zero:

$names = vec['Ada', 'Grace'];
$names[1] = 'Hopper';
$names[] = 'Linus';

assert!($names == vec['Ada', 'Hopper', 'Linus']);

Indexed assignment must replace an existing position. Use $vec[] = $value to append.

The fill form evaluates its value once, then repeats it:

$zeros = vec[0; 4];
assert!($zeros == vec[0, 0, 0, 0]);

Spread a vec or tuple into a vec literal:

$middle = vec[2, 3];
assert!(vec[1, ...$middle, 4] == vec[1, 2, 3, 4]);

A dict cannot spread into a vec.

Dicts

A dict accepts bool, int, and string keys. It keeps insertion order:

$scores = dict['Ada' => 10, 'Grace' => 12];
$scores['Ada'] = 11;
$scores['Linus'] = 9;

assert!($scores['Ada'] == 11);
assert!(length!($scores) == 3);

Keys do not convert. 1, '1', and true are three different keys.

Replacing a value keeps the key’s place. Removing a key and adding it again moves it to the end.

A duplicate literal or spread key keeps its first place and its last value:

$key = 'a';
$values = dict[$key => 1, 'b' => 2, $key => 3];
assert!($values == dict['a' => 3, 'b' => 2]);

A dict spread accepts a tuple, vec, or dict. A tuple or vec contributes integer keys. A dict keeps its keys.

Reading entries

An index must exist. Reading a missing vec position, tuple position, or dict key throws OutOfBoundsError. ?? does not hide that error:

$values = dict['ready' => true];
assert!(!contains_key!($values, 'missing'));

Check with contains_key! before the read. contains! checks values:

$values = vec[10, 20];
assert!(contains_key!($values, 1));
assert!(contains!($values, 20));

length! works on every array and on strings.

Removing entries

remove!($array, $key) removes and returns one entry. On a vec, it preserves order by shifting every later item left. remove_first!($vec) is the same ordered operation at index zero. Use these forms when remaining indexes and iteration order must not change.

Both operations take time in proportion to the items after the removed one. Repeated remove_first! calls therefore take quadratic time. Use Whim\DataStructure\Deque for repeated FIFO removal.

swap_remove!($vec, $index) instead moves the last item into $index. It takes constant time on an unshared vec, but changes order. Use it for unordered work sets and pools. remove_last!($vec) also takes constant time and does not reorder the remaining items.

$values = vec['a', 'b', 'c', 'd'];
$removed = swap_remove!($values, 1);

assert!($removed == 'b');
assert!($values == vec['a', 'd', 'c']);

All vec mutations may first copy storage when another vec shares it.

Iteration

foreach walks keys and values in array order:

$seen = vec[];
foreach (dict['a' => 1, 'b' => 2] as $key => $value) {
  $seen[] = $key . $value;
}

assert!($seen == vec['a1', 'b2']);

The loop binds copies. Assigning $key or $value does not change the array. The loop also keeps the set of entries with which it began, so changing the source during the loop does not change the current walk.

Value Semantics

Whim treats scalars, strings, and arrays as values. Objects and callables have identity.

Array assignment

Assigning an array creates an independent value:

$first = vec[1, 2];
$second = $first;
$second[] = 3;

assert!($first == vec[1, 2]);
assert!($second == vec[1, 2, 3]);

The runtime may share array storage until one value changes. Code cannot observe that sharing.

Array parameters follow the same rule:

function append_zero(vec<int> $values): vec<int> {
  $values[] = 0;
  return $values;
}

$source = vec[1];
$result = append_zero($source);

assert!($source == vec[1]);
assert!($result == vec[1, 0]);

Nested arrays

A write through mutable containers updates the outer value:

$grid = vec[vec[1, 2]];
$grid[0][1] = 20;
assert!($grid == vec[vec[1, 20]]);

A tuple blocks every write through its positions, even when one position holds a vec.

Object identity

Object assignment keeps the same object:

final class Cell {
  public int $value = 0;
}

$first = new Cell();
$second = $first;
$second->value = 42;

assert!($first->value == 42);

Copying an array does not clone the objects inside it:

final class Cell {
  public int $value = 0;
}

$cell = new Cell();
$left = vec[$cell];
$right = $left;
$right[0]->value = 7;

assert!($left[0]->value == 7);

The arrays are distinct values. Both hold the same object.

Closure captures

A short closure captures each outer variable it uses. It captures the current value. An object value still points to the same object:

final class Cell {
  public int $value = 0;
}

$number = 1;
$cell = new Cell();
$read = fn(): (int, int) {
  return ($number, $cell->value);
};

$number = 2;
$cell->value = 3;

assert!($read() == (1, 3));

Lifetime

A value stays alive while a strong reference can reach it. The runtime also finds unreachable cycles.

using, drop!, destructors, weak references, and cycle collection add rules for resources. Resources and Cleanup and References and Cycles cover those rules.

Equality and Order

Whim does not convert either side of an equality check.

Equality

== tests equality. != tests its opposite:

assert!(1 == 1);
assert!(1 != 1.0);
assert!('1' != 1);
assert!(0 != false);
assert!('' != null);

Two vecs or tuples are equal when their kinds, sizes, and ordered values match. Two dicts are equal when they hold the same strict keys and equal values. Dict insertion order does not affect equality:

assert!(vec[1, 2] != vec[2, 1]);
assert!((1, 2) != (2, 1));
assert!(dict['a' => 1, 'b' => 2] == dict['b' => 2, 'a' => 1]);

Objects and callables compare by identity:

final class Token {}

$token = new Token();
$same = $token;

assert!($token == $same);
assert!($token != new Token());

$callable = fn(): int => 1;
assert!($callable == $callable);
assert!($callable != fn(): int => 1);

Enum cases are single values. A case equals itself and not another case.

Float details

Positive and negative floating zero are equal. NaN is not equal to any value, including itself.

Order operators

<, <=, >, and >= compare:

  • int with int or float;
  • float with int or float;
  • string with string, in byte order.

Other pairs throw IncompatibleOperandsError. Arrays, objects, callables, bool, and null have no built-in order.

assert!(1 < 2);
assert!(1 < 2.0);
assert!('apple' < 'banana');
assert!('10' < '9');

A comparison with NaN is false.

Three-way comparison

<=> returns -1, 0, or 1 under the same order rules:

assert!((1 <=> 2) == -1);
assert!((2 <=> 2) == 0);
assert!(('b' <=> 'a') == 1);

It throws when either number is NaN.

Comparison operators do not chain. Write 1 < $value && $value < 10. The parser rejects 1 < $value < 10.

Expressions

Most expressions produce a value. Literals, variables, arrays, operators, calls, member access, object construction, closures, casts, assignments, and match are expressions. return, throw, break, and continue are expressions that do not complete normally.

Variables and constants

Reading an unassigned variable throws UndefinedVariableError. An assignment creates the variable:

$value = 42;
assert!($value == 42);

A constant uses its bare name. A class constant uses :::

const LIMIT = 10;

final class Defaults {
  public const int RETRIES = 3;
}

assert!(LIMIT == 10);
assert!(Defaults::RETRIES == 3);

Object creation

new calls a class constructor:

final class Point {
  public function __construct(public int $x, public int $y) {}
}

$point = new Point(3, 4);
assert!($point->x == 3);

You may omit parentheses when the constructor takes no argument: new Point and new Point() are equal calls.

Use ::<...> between a generic class name and its arguments:

final class Box<T> {
  public function __construct(public T $value) {}
}

$box = new Box::<string>('value');
assert!($box->value == 'value');

A class-name string may drive construction:

final class Token {}

$class = 'Token';
$token = new $class();
assert!($token is Token);

The value must name a declared concrete class. Use classname<T> at a typed boundary when the named class must fit a base type.

Member access

-> reads an instance property or calls an instance method. :: reads a static property, constant, enum case, or static method.

?-> stops when the receiver is null. It returns null and does not evaluate the call arguments:

final class User {
  public function __construct(public string $name) {}
}

$user = null;
assert!($user?->name == null);

The receiver must otherwise be an object of the right type. Whim has no dynamic property creation.

Type tests and casts

is returns a bool:

$value = 42;
assert!($value is int);
assert!(!($value is string));

as returns the same value or throws TypeError:

$value = 42 as int;
assert!($value == 42);

?as returns null instead of throwing:

$value = 'forty-two' ?as int;
assert!($value == null);

Whim keeps types at runtime, so these operators work with unions, ranges, generic types, aliases, newtypes, collection shapes, and symbol types.

Coalescing

$left ?? $right returns the left value unless it is null. It evaluates the right side only for null:

assert!((0 ?? 10) == 0);
assert!((false ?? true) == false);
assert!((null ?? 10) == 10);

The left expression still runs. A missing dict key or bad property access throws before ?? can inspect a value.

Pipeline

|> calls the callable on its right with the left value:

function double(int $value): int {
  return $value * 2;
}

$answer = 21 |> double(...);
assert!($answer == 42);

Both sides run once. A partial callable may choose the input position:

function add(int $left, int $right): int {
  return $left + $right;
}

assert!((3 |> add(?, 4)) == 7);

Whim has no ternary operator. Use if for statements or match for a value.

return

return ends the current function, method, or closure. It may carry the value given back to the caller:

function classify(bool $ready): string {
  return match ($ready) {
    true => 'ready',
    false => return 'waiting',
  };
}

assert!(classify(false) == 'waiting');

A bare return gives back null. A void callable cannot return a value, and a never callable cannot return. return is not valid at file scope or inside a finally block.

Returning runs each active finally block and releases each active using value before the caller resumes.

break and continue

break leaves the nearest loop. continue starts its next pass. Neither produces a value because control leaves the expression.

$values = vec[];
for ($index = 0; $index < 5; $index++) {
  $value = match ($index) {
    1 | 3 => continue,
    4 => break,
    $_ => $index,
  };

  $values[] = $value;
}

assert!($values == vec[0, 2]);

Both run any active finally blocks and release any active using values on the way out.

An integer literal chooses an outer loop:

$count = 0;
for ($x = 0; $x < 3; $x++) {
  for ($y = 0; $y < 3; $y++) {
    $count++;
    break 2;
  }
}

assert!($count == 1);

The level must be greater than zero and cannot exceed the number of enclosing loops. Omitting it means 1.

throw

throw evaluates an object that implements Whim\Unwind\Throwable, then starts unwinding. It does not produce a value because the current path ends. This makes it useful in expressions such as a coalescing fallback:

use Whim\Unwind\RuntimeException;

function cached_or_fail(null|string $cached): string {
  return $cached ?? throw new RuntimeException('value is not cached');
}

assert!(cached_or_fail('ready') == 'ready');

See Throwing and Catching for exception types, catching, and cleanup.

Operators and Arithmetic

Whim checks operator types at runtime. It does not turn strings or booleans into numbers.

Integer arithmetic

+, -, and * return int when both operands are int. Whim checks overflow and underflow:

assert!(2 + 3 == 5);
assert!(2 - 3 == -1);
assert!(2 * 3 == 6);

An out-of-range result throws OverflowError or UnderflowError. Unary -, ++, and -- use the same checks.

If either operand of +, -, or * is float, the result is float:

assert!(1 + 0.5 == 1.5);
assert!(2.0 * 3 == 6.0);

Division and remainder

/ always returns float:

assert!(10 / 2 == 5.0);
assert!(7 / 2 == 3.5);

% accepts ints only. Its result has the sign of the left operand:

assert!(7 % 2 == 1);
assert!(-7 % 2 == -1);

Division or remainder by zero throws DivisionByZeroError, including float division by zero.

Powers

** is right-associative. An int base and a nonnegative int exponent produce an int when the result fits. A negative exponent or any float operand produces a float:

assert!(2 ** 10 == 1024);
assert!(2 ** -1 == 0.5);
assert!(2.0 ** 2 == 4.0);

Integer overflow throws. 0 ** -1 throws DivisionByZeroError.

Bit operators

&, |, ^, ~, <<, and >> accept ints only. A shift count must be from 0 through 63. Left shift uses the 64-bit bit pattern, so its result may wrap from positive to negative.

Increment and decrement

Prefix ++$value changes the target and returns the new value. Postfix $value++ returns the old value. -- follows the same rule. These operators accept int and float targets.

Boolean operators

!, &&, and || accept bool. && skips its right side when the left side is false. || skips it when the left side is true.

Concatenation

. joins strings, ints, and floats as text. It rejects other values.

Comparison and type operators

Equality and Order covers ==, !=, <, <=, >, >=, and <=>.

is, as, and ?as check a runtime type. These operators and comparisons do not chain.

Binding order

The full precedence table appears in the operator appendix. When the order is not plain, use parentheses. They cost nothing and state the intended order.

Assignment and Indexing

Assignment is an expression. It stores a value and returns that value:

$target = 0;
$result = ($target = 5);

assert!($target == 5);
assert!($result == 5);

Targets

An assignment target may be:

  • a variable;
  • an object property;
  • a static property;
  • a vec or dict index;
  • a vec append target, $values[];
  • a tuple or dict destructuring pattern.

Literals, arithmetic results, strings, and tuple entries are not writable targets. A property on an object returned by a call is writable because the object keeps its identity.

Compound assignment

Whim supports these compound forms:

+=  -=  *=  /=  %=  **=  .=
&=  |=  ^=  <<=  >>=  &&=  ||=  ??=

The target expression runs once:

final class Counter {
  public int $calls = 0;

  public function index(): int {
    $this->calls++;
    return 0;
  }
}

$counter = new Counter();
$values = vec[10];
$values[$counter->index()] += 1;

assert!($counter->calls == 1);
assert!($values[0] == 11);

&&=, ||=, and ??= short-circuit like their non-assignment forms.

Tuple and vec destructuring

A tuple target accepts a tuple or vec:

($name, $age) = ('Ada', 36);
assert!($name == 'Ada');
assert!($age == 36);

Without a rest target, the source size must match. A trailing rest target collects the remaining values in a vec:

($head, ...$tail) = vec[1, 2, 3];
assert!($head == 1);
assert!($tail == vec[2, 3]);

Use bare ... to allow and ignore the rest.

Defaulted targets

A default runs only when its position is missing. A present null does not use the default:

($name, $role = 'member') = vec['Ada'];
assert!($role == 'member');

($id, $label = 'fallback') = vec[1, null];
assert!($label == null);

A default may use an earlier binding. Once one target has a default, every later fixed target must also have one. A rest target may follow them.

Dict destructuring

A dict target selects named keys:

$source = dict['id' => 7, 'profile' => dict['name' => 'Ada']];
dict['id' => $id, 'profile' => dict['name' => $name]] = $source;

assert!(($id, $name) == (7, 'Ada'));

Each key expression runs before the source expression. A missing key throws. The source must be a dict.

Write order

Whim reads the full source before it writes any target. A target may reuse the source variable without changing later reads:

$values = (1, 2, 3);
($values, $second, $third) = $values;
assert!(($values, $second, $third) == (1, 2, 3));

Whim writes targets from left to right. If one variable appears twice, the last write wins. $_ follows the same rule because it is an ordinary variable.

Calls and Evaluation Order

Whim evaluates expressions from left to right unless an operator states that it short-circuits.

Calls

Whim evaluates the callable first, then each argument from left to right. For a method call, it evaluates the receiver before the arguments. A constructor uses the same argument order.

final class Log {
  public static vec<string> $events = vec[];
}

function mark(string $name, int $value): int {
  Log::$events[] = $name;
  return $value;
}

function sum(int $a, int $b, int $c): int {
  return $a + $b + $c;
}

assert!(sum(mark('a', 1), mark('b', 2), mark('c', 3)) == 6);
assert!(Log::$events == vec['a', 'b', 'c']);

Operators

Whim evaluates the left operand before the right. An indexed write evaluates the index before the stored value. A dict entry evaluates its key before its value.

Array literal elements, tuple entries, and object constructor arguments also run from left to right.

Short-circuit forms

These forms may skip work:

  • false && $right skips $right;
  • true || $right skips $right;
  • a non-null left side of ?? skips the right side;
  • a null receiver for ?-> skips the member access and call arguments;
  • match evaluates only the chosen arm result;
  • a destructuring default runs only for a missing position.

0, false, and '' are not null, so ?? keeps them.

Pipeline

$value |> $callable evaluates the value, then the callable, once each. It then calls the callable with the value.

Type checks

Whim checks call arguments after it has evaluated them. It checks a return value when the callable returns. A failed check throws TypeError at that boundary.

Default parameter values run when the call omits that argument. Named and partial calls still preserve source evaluation order for the expressions that the caller supplies.

Statements and Loops

Statements run actions and choose control flow.

Expression statements

Any expression may form a statement when followed by ;:

$value = 1;
$value++;
write_line!($value);

Discarding a value marked #[MustUse] raises an error. Use its result or pass it to discard! to state that you meant to ignore it.

An empty statement is one semicolon. It does nothing.

Final locals

final binds a local once:

final $rate = 0.1;
final $price = 250.0;

assert!($price * $rate == 25.0);

The declaration must be the local’s first assignment. Any later write in the same function or file body is a compile error. A final local still follows normal value rules: an object may change through the local, but the local cannot point to another object.

if

An if condition must be bool:

$value = -3;
if ($value < 0) {
  $sign = 'negative';
} else if ($value == 0) {
  $sign = 'zero';
} else {
  $sign = 'positive';
}

assert!($sign == 'negative');

while and do ... while

while tests before each pass. do ... while runs its body once before the first test:

$count = 0;
while ($count < 3) {
  $count++;
}

do {
  $count--;
} while ($count > 0);

assert!($count == 0);

for

A for loop keeps its setup, condition, and step in one header:

$sum = 0;
for ($number = 1; $number <= 5; $number++) {
  $sum += $number;
}

assert!($sum == 15);

Each header section may hold a comma-separated expression list. An empty condition acts as true:

$left = 0;
$right = 3;
for (; $left < $right; $left++, $right--) {
  write_line!($left . ':' . $right);
}

foreach

foreach accepts an array, an Iterator<K, V>, or a ToIterator<K, V>:

$seen = vec[];
foreach (dict['a' => 1, 'b' => 2] as $key => $value) {
  $seen[] = $key . $value;
}

assert!($seen == vec['a1', 'b2']);

Omit the key when it is not needed. The value target may be a destructuring pattern.

Array iteration uses the entries present when the loop begins. Changing the source does not change that walk.

Variable scope

Each file body, function, method, closure, and short closure has its own variable scope. Control-flow blocks do not create another variable scope. A variable assigned on only some paths remains undefined when execution took another path.

Functions cannot read file-scope variables. A closure must capture outer variables, while a short closure captures the outer variables it uses.

Match and Destructuring

match evaluates its subject once. It tests each arm in order, then evaluates the first arm that matches.

Literal patterns

Literal patterns use strict equality. | joins alternatives:

function label(mixed $value): string {
  return match ($value) {
    0 => 'zero',
    1 | 2 | 3 => 'small',
    $_ => 'other',
  };
}

assert!(label(2) == 'small');
assert!(label('2') == 'other');

If no arm matches, Whim throws UnhandledMatchError.

Variable patterns

A variable pattern accepts any value and binds that value inside the selected arm:

$description = match (42) {
  $value => 'value:' . $value,
};

$_ is an ordinary variable. Use it when an arm needs a fallback but does not need to read the value:

$value = null;
$label = match ($value) {
  null => 'none',
  $_ => 'some',
};

assert!($label == 'none');

The binding exists only in its arm. It may shadow an outer variable without changing it.

Type patterns

A type pattern checks the subject with is:

function kind(mixed $value): string {
  return match ($value) {
    int => 'integer',
    string => 'text',
    $_ => 'other',
  };
}

assert!(kind(42) == 'integer');

_ is not a standalone match pattern. It remains valid as an ignored slot in a larger type, such as vec<_>.

Combining patterns with @

left @ right requires both patterns to match the same value. This lets one side bind a value while the other checks it:

function describe(mixed $value): string {
  return match ($value) {
    $number @ int => 'int:' . $number,
    $text @ string => 'string:' . $text,
    $_ => 'other',
  };
}

Both sides may contain nested patterns. Whim performs every check before it creates any binding. A failed arm cannot leave a partial binding or throw due to a missing collection element.

@ takes the union on its right, so this checks either literal and binds the result once:

$small @ 1 | 2

Use parentheses when each union branch has its own pattern tree:

($value @ 1) | ($value @ 2)

Every union branch must bind the same names in the same layout. One pattern cannot bind the same name twice.

Positional patterns

A parenthesized positional pattern matches a tuple or vec. Without ..., its length must match exactly:

function point_name(mixed $value): string {
  return match ($value) {
    ($x @ int, $y @ int) => $x . ',' . $y,
    $_ => 'not a point',
  };
}

assert!(point_name((3, 4)) == '3,4');
assert!(point_name(vec[3, 4]) == '3,4');

A trailing ... permits more values. A pattern after it checks every value in the remainder. A variable after it binds the remainder as a vec:

$total = match (vec[2, 3, 4]) {
  ($first, ...$rest) @ vec<int> => $first + length!($rest),
  $_ => 0,
};

assert!($total == 4);

Intersect the positional pattern with a tuple or vec type when the collection kind matters. Since parenthesized patterns are positional, give a tuple type a name when you need to distinguish it from a vec:

type Point = (int, int);

$point = match ((3, 4)) {
  ($x, $y) @ Point => ($x, $y),
  $_ => null,
};

Vec patterns

vec[...] matches only a vec. Its length is exact unless it ends with ...:

$first = match (vec[1, 2, 3]) {
  vec[$head @ int, ...int] => $head,
  $_ => 0,
};

assert!($first == 1);

Dict patterns

A dict pattern uses literal keys. Without ..., it requires the exact key set. With ..., it permits unlisted keys:

$name = match (dict['id' => 7, 'name' => 'Ada']) {
  dict['name' => $value @ string, ...] => $value,
  $_ => 'unknown',
};

assert!($name == 'Ada');

A missing key rejects the arm. It does not raise OutOfBoundsError.

Patterns may nest on either side of @:

function extract(mixed $value): null|(int, string) {
  return match ($value) {
    dict['foo' => $foo @ 1 | 2, 'bar' => $bar @ !'', ...] @ dict['foo' => int, 'bar' => string, 'baz' => float, ...] => (
      $foo,
      $bar,
    ),
    $_ => null,
  };
}

$value = dict['foo' => 2, 'bar' => 'yes', 'baz' => 1.5];
assert!(extract($value) == (2, 'yes'));

Assignment destructuring

Destructuring assignment uses tuple and dict targets, but it does not test match arms. A mismatch throws. See Assignment and Indexing.

Language Constructs

A language construct looks like name!(...), but it is not a function. The compiler knows its rules and may emit direct bytecode for it.

Array and string constructs

  • length!($value) returns the byte length of a string or the item count of an array.
  • contains!($array, $value) checks array values with strict equality.
  • contains_key!($array, $key) checks an array key or index.
  • remove!($array, $key) removes and returns one entry.
  • swap_remove!($vec, $index) removes and returns one item without preserving vec order.
  • remove_first!($vec) removes and returns the first item.
  • remove_last!($vec) removes and returns the last item.

The remove forms change their target. remove! accepts vecs and dicts. The other forms accept vecs. They throw when no matching item exists. See Removing entries for their order and cost.

Assertions

assert! requires a bool. A false result throws AssertionError:

$value = 42;
assert!($value > 0, 'value must be positive');

The message is optional. The error includes the failed expression and its source location.

Output

  • write!(...) writes values to standard output.
  • write_line!(...) also writes a line ending.
  • write_error!(...) writes to standard error.
  • write_error_line!(...) also writes a line ending.

Each argument must be a string, int, or float.

Debug output

debug!(...) writes a source location and a structural view to standard error. It shows types, string byte lengths, object properties, and collection values. It hides private property values and sensitive callables. It stops after 64 items or 32 nested levels.

debug!(dict['ready' => true, 'count' => 2]);

Use debug! while working, then remove it from normal output paths.

Explicit discard

discard!($value) evaluates and ignores a value. It is the explicit way to ignore a result marked #[MustUse].

Object cloning

clone! copies an object and may replace named properties during the copy:

final readonly class Point {
  public function __construct(public int $x, public int $y) {}

  public function withY(int $y): Point {
    return clone!($this, y: $y);
  }
}

$first = new Point(1, 2);
$second = $first->withY(9);
assert!(($first->y, $second->y) == (2, 9));

Whim checks property visibility, readonly rules, and types during the clone.

Lifetime and process control

drop!($variable) releases one local now. It throws LeakedResourceError if another strong reference keeps a resource alive.

exit!() ends the process with status zero. exit!($status) requires an int and uses its low eight bits. Exit is not an exception: catch cannot catch it, and finally does not run after it.

panic!('message') reports a broken invariant. It takes one literal string, writes panic: message and the current stack trace to standard error, and ends the process with status 255. It is not an exception. catch cannot catch it, and finally does not run after it. Shutdown destructors still run.

if (!contains_key!($states, $name)) {
  panic!('the state table is incomplete');
}

Panic traces hide TraceBoundary frames unless full traces are on. They also hide parameters marked SensitiveParameter. Use throw for errors that a caller may handle. Use panic! only when continuing would be wrong.

Source paths and loading

  • file!() returns the current source file path.
  • directory!() returns its directory.
  • embed!('./file') reads a file while compiling.
  • require!($path) loads and runs a source file.
  • require_once!($path) does that at most once per resolved path.

See Loading Files for load and error rules.

Compile-time file embedding

embed! takes one literal relative path. Whim resolves it from the directory of the source file that contains the construct, reads the file while compiling, and stores its exact bytes as a string:

const TEMPLATE = embed!('./template.html');

Whim does not decode text or change line endings. The running program does not read the file. A source read from standard input cannot use embed! because it has no directory. Absolute paths, missing files, unreadable files, and directories cause compile errors.

The compiler reads an embedded file even when the construct appears in code that will not run. It reads each resolved path once per compilation. Do not use embed! for secrets: the bytes remain plain in bytecode and compiled artifacts.

Functions

A function declaration has a name, optional type parameters, parameters, an optional return type, and a body.

function area(float $width, float $height): float {
  return $width * $height;
}

assert!(area(4.0, 2.5) == 10.0);

Parameter and return types

Whim checks each typed argument before the function starts. It checks a typed result before the caller receives it.

function identity<T>(T $value): T {
  return $value;
}

assert!(identity::<string>('value') == 'value');

An omitted parameter or return type means mixed. Write the type when the function has a useful contract.

A void function returns no value:

function announce(string $message): void {
  write_line!($message);
}

announce('ready');

A never function cannot return:

function fail(string $message): never {
  throw new Whim\Unwind\RuntimeException($message);
}

Optional parameters

A default makes a parameter optional:

function greet(string $name, string $greeting = 'Hello'): string {
  return $greeting . ', ' . $name;
}

assert!(greet('Ada') == 'Hello, Ada');
assert!(greet('Ada', 'Welcome') == 'Welcome, Ada');

Required parameters must come before optional parameters. Whim evaluates an omitted default when the call enters the function. It then checks the default against the parameter type.

Defaults use constant expressions. They may use literals, arrays, constants, named object construction, and calls whose receiver and arguments are also constant expressions.

Named arguments

An argument may name its parameter:

function box(string $label, int $width = 3, int $height = 1): string {
  return $label . ':' . $width . ':' . $height;
}

assert!(box('panel', height: 9) == 'panel:3:9');
assert!(
  box(
    height: 2,
    label: 'card',
    width: 4,
  )
  == 'card:4:2',
);

Named arguments may skip optional parameters and may appear out of declaration order. An unknown or repeated name throws ArgumentCountError.

Argument count

Whim has no implicit variadic parameter. A function receives exactly its declared parameters. Use a vec when a call should pass a list:

function sum(vec<int> $values): int {
  $total = 0;
  foreach ($values as $value) {
    $total += $value;
  }

  return $total;
}

assert!(sum(vec[1, 2, 3]) == 6);

Too few or too many arguments throw ArgumentCountError.

Local scope

Parameters and assignments belong to the function call. A function cannot read variables from the file that declared it. Use parameters, constants, static properties, or a closure capture to pass data in.

Calls may recurse. [runtime].call-depth sets the frame limit. The WHIM_CALL_DEPTH environment variable overrides it for one run. Exceeding the limit throws StackOverflowError.

Generic functions

A function may declare reified type parameters:

function singleton<T>(T $value): vec<T> {
  return vec[$value];
}

assert!(singleton::<int>(7) is vec<int>);

Whim does not infer T from an argument. Supply ::<...> or give T a default. The Generics chapter covers bounds, defaults, variance, and forwarding.

Closures

A callable is a closure, short closure, first-class function, bound method, or partial call.

Short closures

A short closure uses fn. This is the preferred closure syntax:

$double = fn(int $value): int {
  return $value * 2;
};

assert!($double(21) == 42);

Like a named function, a short closure may have type parameters, typed parameters, defaults, a return type, attributes, and a block body.

A short closure may also have an expression body:

$factor = 3;
$multiply = fn(int $value): int => $value * $factor;

assert!($multiply(4) == 12);

A block body may contain any statements. It does not return its last expression. Use return to return a value. A block body may declare void; an expression body may not.

A short closure captures each outer variable that its body uses. Capture is by value at creation time. Parameters are not captures.

Explicit captures

A long closure uses function and lists outer variables in use:

$offset = 10;
$add = function(int $value) use ($offset): int {
  return $value + $offset;
};

assert!($add(5) == 15);

Capture copies the current value. A later assignment to the outer variable does not change that copy. Mutating a captured local also does not change the outer local.

Objects keep identity when copied, so a captured object still sees later property changes.

Use a long closure when the explicit capture list helps the reader. Prefer fn otherwise.

$this

A closure or short closure made in an instance method may use $this without listing it in use:

final class Counter {
  public function __construct(private int $value) {}

  public function reader(): fn(): int {
    return fn(): int => $this->value;
  }
}

$counter = new Counter(7);
assert!($counter->reader()() == 7);

The callable keeps the receiver alive.

Callable types

fn(int, string): bool describes a callable by its input and output types. Whim checks callable compatibility when a value crosses a typed boundary, then checks each call as it runs.

Parameter types are contravariant and the return type is covariant. A callable that accepts mixed may replace one that accepts int. A callable that returns int may replace one that returns int|string.

Callable values compare by identity. Two closures with the same source are still different values.

First-Class and Partial Calls

Whim can turn a known function or method into a callable without wrapping it in a closure.

First-class functions

Write (...) in place of the argument list:

function square(int $value): int {
  return $value * $value;
}

$square = square(...);
assert!($square(9) == 81);

For a generic function, put the type arguments first:

function identity<T>(T $value): T {
  return $value;
}

$read = identity::<string>(...);
assert!($read('value') == 'value');

Bound methods

A first-class instance method keeps its receiver:

final class Greeter {
  public function __construct(private string $name) {}

  public function greet(string $prefix): string {
    return $prefix . $this->name;
  }
}

$greeter = new Greeter('Ada');
$greet = $greeter->greet(...);
assert!($greet('Hello, ') == 'Hello, Ada');

Static methods use ClassName::method(...). Whim checks visibility when it creates the callable.

Partial calls

? leaves one argument open:

function join(string $left, string $middle, string $right): string {
  return $left . $middle . $right;
}

$wrap = join('(', ?, ')');
assert!($wrap('value') == '(value)');

Whim evaluates bound argument expressions when it creates the partial. Later calls reuse those values.

Several holes become parameters in the order in which the partial expression lists them:

function format(int $id, string $label, bool $loud): string {
  return $label . ':' . $id;
}

$render = format(label: ?, loud: true, id: ?);
assert!($render('item', 7) == 'item:7');

The first new parameter fills label; the second fills id.

Leaving later parameters open

A trailing ... leaves other unbound parameters open:

function shape(string $kind, int $size = 1, string $mode = 'flat'): string {
  return $kind . ':' . $size . ':' . $mode;
}

$deep = shape(?, mode: 'deep', ...);
assert!($deep('cube', 4) == 'cube:4:deep');

Without the trailing ..., parameters without holes keep their defaults or no longer belong to the partial callable.

A partial callable may itself be partially called. Whim preserves the bound values and the order of the remaining holes.

Classes and Properties

A class defines objects with identity. Two variables can point to the same object, and a change through either variable changes that object.

class Counter {
  public int $value = 0;

  public function increment(): void {
    $this->value++;
  }
}

$first = new Counter();
$second = $first;
$second->increment();
write_line!($first->value); // 1

Declaring a class

A class body may contain properties, constants, and methods. Every member must state its visibility: public, protected, or private.

class User {
  public const string KIND = 'user';
  private static int $created = 0;

  public function __construct(public readonly int $id, private string $name) {
    self::$created++;
  }

  public function rename(string $name): void {
    $this->name = $name;
  }

  public function label(): string {
    return $this->id . ': ' . $this->name;
  }

  public static function created(): int {
    return self::$created;
  }
}

Properties use $ in their names. Methods and constants do not. A property may share a name with a method or constant. Methods and constants may not share a name with each other. An enum case also uses that member name set.

Construction

new creates an object, then calls __construct when the class has one. The call site must have access to that constructor. Code outside the class can call only a public constructor.

final class Point {
  public function __construct(public int $x, public int $y) {}
}

$point = new Point(3, 4);

A visibility modifier on a constructor parameter promotes that parameter to a property. The property keeps the parameter’s type and readonly modifier.

Without promotion, the constructor assigns properties through $this:

final class Name {
  private string $value;

  public function __construct(string $value) {
    $this->value = $value;
  }

  public function value(): string {
    return $this->value;
  }
}

Named arguments work with constructors:

final class Entry {
  public function __construct(public int $number, public string $text) {}
}

$entry = new Entry(
  text: 'answer',
  number: 42,
);

A child constructor does not call its parent on its own. Call parent::__construct(...) when the parent needs setup.

A private constructor can force callers through a factory:

final class Token {
  private function __construct(public string $value) {}

  public static function from(string $value): Token {
    return new Token($value);
  }
}

$token = Token::from('ready');

Property state

A property may have a default value:

class Job {
  public string $state = 'waiting';
}

A property without a default starts uninitialized. Reading it throws Whim\Unwind\UninitializedPropertyError.

class Job {
  public string $state;
}

$job = new Job();
$state = $job->state; // throws

An initializer may call functions or methods and may create objects. Whim evaluates a non-static property initializer for each new object. Objects do not share that value.

final class Box {
  public function __construct(public int $value) {}
}

final class Holder {
  public Box $box = new Box(1);
}

$first = new Holder();
$second = new Holder();
$first->box->value = 9;
write_line!($second->box->value); // 1

Every write checks the property’s type. A compound assignment also leaves the old value in place when its operation or type check fails.

Readonly properties

A readonly property accepts one write. The write must occur in code that can access the property. Later writes throw Whim\Unwind\ReadonlyError.

class Account {
  public readonly int $id;

  public function __construct(int $id) {
    $this->id = $id;
  }
}

A child constructor may initialize an inherited public or protected readonly property. It may not initialize a private parent property.

readonly class makes each instance property readonly, including promoted properties that omit the word readonly.

readonly class Pair {
  public function __construct(public int $left, public int $right) {}
}

A readonly class cannot have static properties. A readonly class may extend only another readonly class, and a child of a readonly class must also be readonly.

Static properties and class constants

Static properties belong to the class family, not to one object. An inherited static property uses the same stored value.

class Registry {
  public static int $count = 0;
}

class ChildRegistry extends Registry {}

ChildRegistry::$count = 3;
write_line!(Registry::$count); // 3

A class constant has a type and a value:

final class Limits {
  public const int MAXIMUM = 100;
}

write_line!(Limits::MAXIMUM);

final prevents a child from replacing a class constant.

Constant values may call functions and methods or create objects. Whim checks them when it declares the class.

$this, self, parent, and static

$this is the current object in an instance method.

self names the class that declares the current method. parent names its direct parent. static names the class on which the call began, so it supports late static dispatch.

class Model {
  public static function create(): static {
    return new static();
  }

  public function kind(): string {
    return 'model';
  }
}

final class Post extends Model {
  public function kind(): string {
    return 'post';
  }
}

write_line!(Post::create()->kind()); // post

Use self, not static, as a parameter type. A return type may use either.

Abstract and final classes

An abstract class may hold abstract methods. An abstract method has no body. A concrete child must implement every abstract method before code can create it.

abstract class Shape {
  abstract public function area(): float;
}

final class Square extends Shape {
  public function __construct(public float $side) {}

  public function area(): float {
    return $this->side * $this->side;
  }
}

Code cannot create an abstract class. Code cannot extend a final class or replace a final method.

final abstract class has a narrow use: it groups static code. It may contain only constants, static properties, and concrete static methods.

final abstract class Numbers {
  public const int ONE = 1;

  public static function one(): int {
    return self::ONE;
  }
}

Cloning

clone!($object) creates a new object with copies of the source properties. The two objects then have separate property slots.

final class Point {
  public function __construct(public int $x, public int $y) {}
}

$source = new Point(1, 2);
$copy = clone!($source, y: 9);
$source->x = 7;

write_line!($copy->x); // 1
write_line!($copy->y); // 9

Named fields after the object replace properties on the clone. Normal visibility and readonly rules apply at the call site. A wrong field name or a non-object source throws TypeError. Some built-in classes reject cloning. Whim has no clone hook.

Destruction

A class may declare __destruct(): void. Whim calls it when no strong reference can reach the object, including when cycle collection finds an unreachable cycle.

final class Lease {
  public function __destruct(): void {
    write_line!('released');
  }
}

$lease = new Lease();
$lease = null;

A child inherits its parent’s destructor unless it declares one. A child destructor replaces the parent destructor; call parent::__destruct() to run both.

A destructor may throw. During normal execution, the throw starts at the statement that removed the last reference. During shutdown, a destructor failure becomes the program’s failure. Keep destructors short and make cleanup safe to call more than once.

Inheritance and Visibility

A class may extend one class and implement any number of interfaces.

interface Named {
  public function name(): string;
}

class Entity {
  public function __construct(public readonly int $id) {}
}

final class User extends Entity implements Named {
  public function __construct(int $id, private string $label) {
    parent::__construct($id);
  }

  public function name(): string {
    return $this->label;
  }
}

Whim rejects class and interface inheritance cycles.

Dynamic method dispatch

An instance call uses the method on the object’s runtime class. This remains true when a parent method makes the call.

class Animal {
  public function describe(): string {
    return 'a ' . $this->kind();
  }

  protected function kind(): string {
    return 'animal';
  }
}

final class Dog extends Animal {
  protected function kind(): string {
    return 'dog';
  }
}

write_line!(new Dog()->describe()); // a dog

parent::method() calls the parent implementation directly.

Visibility

Any scope may use a public member.

The declaring class, its parents, and its children may use a protected member. This access works both ways within one inheritance family. It does not grant access to an unrelated class.

private code belongs only to the class that declares it. A child cannot use its parent’s private member.

Private properties and methods do not take part in normal overriding. A child may declare its own private member with the same name. Parent code still uses the parent’s member, and child code uses the child’s member.

class ParentValue {
  private string $value = 'parent';

  public function parentValue(): string {
    return $this->value;
  }
}

class ChildValue extends ParentValue {
  private string $value = 'child';

  public function childValue(): string {
    return $this->value;
  }
}

Method overrides

An overriding method must keep a compatible contract:

  • It cannot add required parameters.
  • It may add optional parameters.
  • It may widen visibility, such as protected to public.
  • It may not change an instance method to static or a static method to an instance method.
  • It may not replace a final method.
  • Its parameter and return types must meet the inherited type contract.

Whim checks these rules when it links the classes. An abstract child may leave an inherited abstract method open. A concrete child may not.

Property inheritance

Property types are invariant. A child that redeclares an inherited public or protected property must use the same type. It must also keep the property readonly or writable as declared.

A child may redeclare a compatible inherited property. The object still has one slot for that inherited property.

A private parent property is a different slot from a child property with the same name.

Constant inheritance

A child inherits visible class and interface constants. A class constant may replace an inherited class constant with a narrower type:

class Broad {
  public const int|string VALUE = 1;
}

class Narrow extends Broad {
  public const int VALUE = 2;
}

A child cannot replace a final constant.

An inherited method and constant cannot share a name. A class also cannot replace an interface constant it implements.

Generic base types

Type arguments form part of the inherited contract:

interface Source<out T> {
  public function value(): T;
}

final class IntegerSource implements Source<int> {
  public function value(): int {
    return 1;
  }
}

The number of base type arguments must match. Each argument must meet its bound. A class cannot implement incompatible forms of the same generic interface.

Sealed families

The for clause limits direct children or implementors.

interface Vehicle for Motorized, Towed {}
interface Motorized extends Vehicle for Car {}
interface Towed extends Vehicle for Trailer {}

final class Car implements Motorized {}
final class Trailer implements Towed {}

Only Motorized and Towed may directly extend or implement Vehicle. Only Car may directly extend or implement Motorized. Only Trailer may directly extend or implement Towed.

Permission continues through a listed child. Car is a Vehicle because it implements Motorized. The child controls which symbols may sit directly below it.

A sealed class uses the same form:

abstract class Event for Login {}
class Login extends Event {}
final class DetailedLogin extends Login {}

The linker enforces the family even when its members load from separate files.

Interfaces and Sealed Families

An interface states which members an object provides. It may require methods, constructors, properties, and constants.

interface Record {
  public const string KIND = 'record';
  public readonly int $id;

  public function label(): string;
}

final class User implements Record {
  public function __construct(public readonly int $id) {}

  public function label(): string {
    return self::KIND . ' ' . $this->id;
  }
}

Every interface member states its visibility. Interface properties cannot have storage or default values. A constructor parameter in an interface cannot use property promotion because the interface cannot declare storage.

Method requirements

A method without a body is a requirement:

interface Writable {
  public function write(string $bytes): void;
}

A concrete class must supply a compatible method. Staticness, visibility, parameters, return type, and generic parameters form part of that contract.

An interface may give a method a body. A class then inherits that default unless it supplies its own compatible method.

interface Named {
  public readonly string $name;

  public function displayName(): string {
    return $this->name;
  }
}

final class Person implements Named {
  public function __construct(public readonly string $name) {}
}

write_line!(new Person('Ada')->displayName());

Constructor requirements

An interface can require a constructor:

interface Buildable {
  public function __construct(int $id, int $revision);
}

final class Widget implements Buildable {
  public function __construct(public int $id, public int $revision) {}
}

The class constructor must accept every call allowed by the interface constructor.

Property requirements

An interface may require a public property:

interface MutableName {
  public string $name;
}

interface FixedName {
  public readonly string $name;
}

Property types are invariant. A class that implements MutableName must expose a writable string property. A class that implements FixedName must expose a readonly string property.

Constants

An interface constant has a value. Implementing classes inherit it.

interface Format {
  public const string NAME = 'json';
}

final class JsonFormat implements Format {}

write_line!(JsonFormat::NAME);

An implementing class cannot redeclare that constant. Whim also rejects a class that inherits conflicting constants from two interfaces.

Extending interfaces

An interface may extend more than one interface:

interface HasId {
  public readonly int $id;
}

interface HasLabel {
  public function label(): string;
}

interface Entity extends HasId, HasLabel {}

The child interface contains all inherited contracts. A class must satisfy them as one set. Whim rejects inherited method and constant name conflicts.

self in a contract

self in an interface method resolves to the implementing class at runtime.

interface Copyable {
  public function copy(): self;
}

final class Item implements Copyable {
  public function copy(): self {
    return $this;
  }
}

Generic interfaces

Interfaces may have reified type parameters and variance:

interface Source<out T> {
  public function read(): T;
}

interface Sink<in T> {
  public function write(T $value): void;
}

out T may appear only in output positions. in T may appear only in input positions. An unmarked type parameter is invariant. The generics chapter gives the full rules.

Sealed interfaces

An interface can list the symbols allowed to sit directly below it:

interface Outcome for Success, Failure {}
final class Success implements Outcome {}
final class Failure implements Outcome {}

Any other direct implementor or child interface fails to link. Listed child interfaces may define their own lists. See Inheritance and Visibility for the full rule.

Enums

An enum defines a fixed set of values. Each case is one shared value.

enum Direction {
  case North;
  case South;
}

$direction = Direction::North;
write_line!($direction->name); // North

Code cannot create an enum with new and cannot clone an enum case.

Unit enums

An enum without a backing type is a unit enum. Its cases have names but no backing values.

enum State {
  case Waiting;
  case Running;
  case Done;
}

Every enum case has a public readonly name property. Every enum has a static cases() method that returns its cases in source order.

enum State {
  case Waiting;
  case Running;
  case Done;
}

$states = State::cases();
write_line!($states[0]->name); // Waiting

All enums implement Whim\Enum\UnitEnum.

Backed enums

A backed enum uses int or string. A type alias that resolves to one of those types also works.

enum Status: string {
  case Ready = 'ready';
  case Waiting = 'waiting';
}

enum Code: int {
  case Ok = 200;
  case Missing = 404;
}

Every case must have a value, and no two cases may have the same value. A backed case has a public readonly value property.

Backed enums also implement Whim\Enum\BackedEnum<int> or Whim\Enum\BackedEnum<string>.

Looking up backed cases

from returns the case with a given value. It throws Whim\Unwind\ValueError when no case has that value.

tryFrom returns the case or null.

enum Status: string {
  case Ready = 'ready';
  case Waiting = 'waiting';
}

$ready = Status::from('ready');
$missing = Status::tryFrom('missing');

write_line!($ready->name);
assert!($missing == null);

Methods and interfaces

An enum may implement interfaces and define concrete methods.

interface Labelled {
  public function label(): string;
}

enum Level: int implements Labelled {
  case Low = 1;
  case High = 10;

  public function label(): string {
    return match ($this) {
      self::Low => 'low',
      self::High => 'high',
    };
  }
}

An enum cannot:

  • extend a class or enum;
  • declare type parameters;
  • declare properties or a constructor;
  • declare abstract methods;
  • replace its built-in enum methods.

Its methods, constants, and cases share one member name set.

Matching enum cases

An enum case is also a type that matches only that case. It can appear directly in a match arm:

enum Status: string {
  case Ready = 'ready';
  case Waiting = 'waiting';
}

function label(Status $status): string {
  return match ($status) {
    Status::Ready => 'ready now',
    Status::Waiting => 'not yet',
  };
}

Whim does not require a match over an enum to list every case at compile time. Code can load types after the first file compiles. If no arm matches at runtime, Whim throws Whim\Unwind\UnhandledMatchError.

Runtime Type Checks

Whim keeps type data while a program runs. It checks types at each boundary that has a declared type.

function double(int $value): int {
  return $value * 2;
}

assert!(double(4) == 8);

Calling double('4') throws Whim\Unwind\TypeError. Whim does not turn the string into an integer.

Where Whim checks types

Whim checks:

  • function and method arguments;
  • return values;
  • property defaults and writes;
  • static properties and constants;
  • enum backing values;
  • generic type arguments and their bounds;
  • collection writes when a typed place holds the collection;
  • is, as, ?as, match patterns, and typed catch clauses;
  • class, interface, and override contracts.

The compiler may prove that a check will pass and remove it. This changes no result. A check remains when the compiler cannot prove the type.

Type errors do not change the old value

Whim prepares a write, runs the operation, checks the result, then stores it. If any step throws, the old place stays unchanged.

class Counter {
  public int $value = 1;
}

$counter = new Counter();
$counter->value = 'wrong'; // throws; value remains 1

This rule also covers nested array writes and compound assignments.

is

$value is T returns true when the value fits T.

function describe(int|string $value): string {
  if ($value is int) {
    return 'integer ' . $value;
  }

  return 'string ' . $value;
}

The value expression runs once. The type may contain aliases, ranges, collections, type parameters, and symbol types.

An unknown name may run the autoloader. If the name remains unknown, the check returns false instead of throwing. A later declaration can make a later check match.

Whim permits some bare generic names in a class test:

final class Box<T> {}

$box = new Box::<string>();
assert!($box is Box);
assert!($box is Box<_>);

Built-in collection names need their full number of type arguments in a runtime check. Write vec<_>, dict<_, _>, or array<_, _>, not bare vec, dict, or array.

as

$value as T checks T and returns the same value. It throws TypeError on a failed check.

$value = 42 as int;

The cast does not convert scalar values. 1 as float fails.

A cast to a newtype adds that newtype’s tag after checking the backing type.

?as

$value ?as T returns the checked value or null.

$raw = 'not an integer';
$timeout = ($raw ?as int) ?? 30;

This form does not throw for a type mismatch. It may still throw while evaluating $value.

Deep checks

Collection and generic checks inspect their parts when the type calls for it.

$values = vec[1, 2];
assert!($values is vec<int>);

$values[] = 'changed';
assert!(!($values is vec<int>));
assert!($values is vec<int|string>);

The same rule applies to dict keys and values, tuple positions, shape types, object type arguments, and newtype backing values.

An empty vec has type vec<never>. An empty dict has type dict<never, never>. Since never fits every type, empty collections fit any compatible element type.

Reified generic types

An object keeps its type arguments:

final class Box<T> {
  public function __construct(public T $value) {}
}

$box = new Box::<int>(1);
assert!($box is Box<int>);
assert!(!($box is Box<string>));

Function and method calls also keep their bound type arguments for the length of the call. Code may test a value against a type parameter with is T.

Relative class types

Inside a class:

  • self is the declaring class with its bound type arguments;
  • parent is its direct parent;
  • static is the runtime class on which the call began.

static may appear as a return type but not a parameter type. Use self for a parameter that accepts the declaring class and its children.

Type identifiers

Whim\Type gives code an opaque integer key for a type.

use Whim\Type;

function same_type<T>(T $value): bool {
  return Type\of($value) == Type\id::<T>();
}

assert!(same_type::<int>(42));

Type\of($value) describes the value’s runtime type. Type\id::<T>() describes T. Equal types have equal identifiers in one engine.

Use a type identifier only for equality or as an array key. Its integer value has no public meaning. Do not store it for another process or run.

Loading and checks

Whim can compile a type name before that symbol loads. Once an autoloader or a file declares the symbol, later checks use its real type. This is why Whim does not require all type names to exist when it first parses a file.

Links between declared classes and interfaces are stricter. Whim checks their inheritance and member contracts when it links them.

Unions, Intersections, and Ranges

Whim builds larger types from smaller ones. These operators describe sets of values. They do not convert values.

Union types

A|B accepts a value that fits A or B.

type Number = int|float;

function square(Number $value): Number {
  return $value * $value;
}

A union may contain any valid member except void or never. never adds no value, so Whim rejects it as a redundant union member. mixed already contains every value, so Whim rejects other members beside it.

Direct duplicate or covered members are also errors:

int|int
bool|true

Aliases can hide that two written members are equal. Whim still gives the union the right runtime meaning.

Intersection types

A&B accepts a value that fits both A and B.

interface Named {}
interface Stored {}

function save(Named&Stored $value): void {}

An intersection can combine class and interface contracts or refine any type:

type NonEmptyString = string&!'';
type SmallPositiveInt = int&1..=100;

mixed adds no rule to an intersection, so Whim rejects it there. A direct duplicate member is also an error.

An impossible intersection is a valid empty type. For example, int&SomeInterface has no value unless the two parts can overlap.

Negated types

!T accepts every value outside T.

function require_value(!null $value): !null {
  return $value;
}

Negation works inside collections, callable types, bounds, catch clauses, and other composed types.

$values = vec[1, 'text'];
assert!($values is vec<!bool>);

Useful identities include:

  • !never accepts every value;
  • !mixed accepts no value;
  • !!T has the same values as T.

Whim does not allow negation of void or the wildcard _.

Precedence

Prefix ! and = bind first. & binds before |.

A&B|C       means (A&B)|C
A|B&C       means A|(B&C)
!(A|B)      excludes the whole union

Use parentheses when they make the type easier to read.

Literal types

An integer, float, string, boolean, null, enum case, or constant can describe one value.

type Answer = 'yes'|'no';
type Ordering = -1|0|1;

function enabled(true $value): void {}

Integer and float literal types remain distinct. 1 does not contain 1.0.

Integer range types

Range types accept integer intervals.

TypeAccepted values
1..101 through 9
1..=101 through 10
0..zero and all larger integers
..0all integers below zero
..=0zero and all smaller integers

The lower bound is inclusive. .. excludes the upper bound, while ..= includes it.

type Port = 1..=65535;
type Offset = 0..;

function connect(Port $port): void {}

Both bounds may be negative. A reversed or equal exclusive range is empty. 1..=1 contains only 1.

Ranges work as generic bounds and type arguments:

function clamp_input<T: 0..=100>(T $value): T {
  return $value;
}

assert!(clamp_input::<25>(25) == 25);

never

never contains no values. A function that returns never must throw, exit, or keep running.

function fail(string $message): never {
  throw new Whim\Unwind\RuntimeException($message);
}

never may appear in parameters and generic types. Since no caller can supply a value of that type, it helps express branches that cannot run. A method that returns never can satisfy a contract with any return type.

void

void is only a return type. A void function returns no value.

void cannot appear in a parameter, property, union, negation, type argument, or type alias.

Collection and Callable Types

Whim can describe the parts of arrays and callables at runtime.

Homogeneous vecs

vec<T> accepts a vec whose every value fits T.

function total(vec<int> $values): int {
  $sum = 0;
  foreach ($values as $value) {
    $sum += $value;
  }

  return $sum;
}

The vec’s runtime element type changes as code adds or removes values. An empty vec has element type never, so it fits vec<T> for any T.

Bare vec accepts a vec without checking its items at a declared boundary. The matching forms is vec, as vec, and a bare vec match pattern are not valid; write vec<_> when an explicit runtime check may accept any items.

Vec shapes

vec[T0, T1] describes fixed positions and an exact length.

function pair(vec[int, string] $value): void {}

pair(vec[42, 'answer']);

A final ...T allows zero or more extra values of T:

type Row = vec[string, ...int];

assert!(vec['row'] is Row);
assert!(vec['row', 1, 2] is Row);

Homogeneous dicts

dict<K, V> checks every key against K and every value against V.

function scores(dict<string, int> $scores): void {}

scores(dict['Ada' => 10, 'Grace' => 12]);

Dict keys can be int, string, or bool. The type may use one of them, a union, a range, or another type that fits those key kinds.

An empty dict has type dict<never, never> and fits every valid dict key and value type.

Bare dict and array follow the same boundary rule. In an explicit runtime check, use dict<_, _> or array<_, _>.

Dict shapes

A dict shape lists required keys and their value types.

type UserRow = dict['id' => int, 'name' => string];

$user = dict['id' => 1, 'name' => 'Ada'];
assert!($user is UserRow);

Without a rest entry, the dict must have only the listed keys. A rest entry allows other keys and gives them a key and value type:

type ScoredUser = dict['id' => int, 'name' => string, ...<string, int|float>];

$user = dict['id' => 1, 'name' => 'Ada', 'score' => 9.5];
assert!($user is ScoredUser);

Tuple types

(A, B) describes an exact tuple length and each position.

type Coordinate = (float, float);

function move(Coordinate $point): Coordinate {
  return ($point[0] + 1.0, $point[1] + 1.0);
}

A one-item tuple type has a trailing comma: (T,).

A final ...T accepts zero or more trailing items of T:

type Delivery = (int, int, ...string);

assert!((41, 99) is Delivery);
assert!((41, 99, 'fragile', 'signed') is Delivery);

The rest item must be last. Omitting its type, as in (int, ...), uses mixed. A tuple has at least one and at most twelve fixed items.

The common array type

array<K, V> accepts a tuple, vec, or dict whose keys fit K and values fit V.

function count_values(array<_, _> $values): int {
  return length!($values);
}

assert!(count_values(vec[1, 2]) == 2);
assert!(count_values(dict['one' => 1]) == 1);
assert!(count_values((1, 'two')) == 2);

For a vec, keys are non-negative integers. For a tuple, keys form the range of its positions. For a dict, keys keep their declared kinds.

array<K, V> is a read-only type view. It does not change the value into a new array form.

Callable types

fn(A, B): R describes a callable that accepts A and B and returns R.

function apply(fn(int): string $format, int $value): string {
  return $format($value);
}

$result = apply(fn(int $value): string => 'n=' . $value, 42);

A leading = marks an optional parameter in a callable type:

fn(int, =string): bool

This type accepts a callable that lets callers omit its second argument.

Bare fn accepts any callable.

Callable parameters are contravariant. A callable that accepts mixed can stand in for one that needs only int. Return types are covariant. A callable that returns int can stand in for one that may return int|string.

Callable values also carry their generic binding and origin. The symbols as types chapter covers specific function and method families.

Class-name types

classname<T> accepts a string that names a class whose instances fit T.

interface Shape {}
class Circle implements Shape {}

function make(classname<Shape> $class): Shape {
  return new $class();
}

$shape = make('Circle');

The string may include reified type arguments, such as Box<int>. A child class name also fits a parent or interface bound. A missing class or wrong type argument fails the check.

The inner type must be able to contain a class-like type. classname<int> is invalid.

Wildcards

_ means that a nested type exists but its fixed value does not matter.

assert!(vec[1, 'two'] is vec<_>);
assert!(dict['one' => 1] is dict<_, _>);

It can select some generic arguments while keeping others fixed:

final class Pair<A, B> {}

$pair = new Pair::<string, int>();
assert!($pair is Pair<_, int>);

It also works in tuple positions and callable parameters.

_ cannot stand alone as a type. Whim does not allow !_. It also does not mean mixed: Box<_> accepts a Box<string>, while Box<mixed> follows the class’s variance and may reject it.

Generics

Whim generics keep their type arguments at runtime. Classes, interfaces, functions, methods, closures, aliases, and newtypes may declare type parameters.

final class Box<T> {
  public function __construct(public T $value) {}
}

function identity<T>(T $value): T {
  return $value;
}

$box = new Box::<int>(42);
$value = identity::<string>('Whim');

Declaring type parameters

Type parameters appear between < and > after a symbol name.

final class Pair<A, B> {
  public function __construct(public A $first, public B $second) {}
}

Each name must be unique in that list. A nested generic declaration may add its own parameters:

final class Box<T> {
  public function __construct(public T $value) {}

  public function map<U>(fn(T): U $transform): Box<U> {
    return new Box::<U>($transform($this->value));
  }
}

Supplying type arguments

A type annotation uses ordinary angle brackets:

Box<int>
dict<string, Box<int>>

A constructor or call uses ::<...>:

final class Box<T> {
  public function __construct(public T $value) {}
}

function identity<T>(T $value): T {
  return $value;
}

$box = new Box::<int>(1);
$value = identity::<int>(2);

The number of arguments must match the number of parameters that lack defaults. Whim does not infer a missing type argument from a value. Supply it or declare a default.

This applies to functions, methods, static methods, closures, and arrows.

$identity = fn<T>(T $value): T {
  return $value;
};

$value = $identity::<string>('text');

Runtime reification

The type argument remains available during the call and in the object.

function matches<T>(mixed $value): bool {
  return $value is T;
}

assert!(matches::<vec<int>>(vec[1, 2]));
assert!(!matches::<vec<int>>(vec[1, 'two']));

An object test includes its type arguments:

final class Box<T> {}

$box = new Box::<int>();
assert!($box is Box<int>);
assert!(!($box is Box<string>));

A generic class may use its parameter in properties, methods, parent types, and implemented interfaces. Every write and call keeps that binding.

Defaults

= Type gives a type parameter a default.

final class Box<T = int> {}

function identity<T = int>(T $value): T {
  return $value;
}

$box = new Box();
$value = identity(42);

Whim applies the default when the caller omits the argument. The default must meet the parameter’s bound. A default cannot depend on its own parameter before that parameter has a binding.

Defaults also let a generic callable fit a non-generic callable type. A generic callable with no defaults still needs type arguments when invoked.

Bounds

T: Bound limits the type arguments accepted for T.

interface Named {
  public function name(): string;
}

function label<T: Named>(T $value): string {
  return $value->name();
}

Whim checks each supplied or defaulted type argument against the bound. Bounds can use any type expression, including unions, ranges, negation, constants, and other type parameters.

Use + when one parameter must meet several bounds:

interface Named {}
interface Stored {}

function save<T: Named + Stored>(T $value): void {}

A bound may depend on another parameter:

type Weaken<T: W, W> = W;

Here T must fit W, and the alias exposes only W at runtime.

Constructing a type parameter

Code may create a reified type parameter when its bound supplies a constructor.

interface Constructable {
  public function __construct();
}

function create<T: Constructable>(): T {
  return new T();
}

A bound may also supply static methods:

interface Buildable {
  public static function build(int $seed): static;
}

function build<T: Buildable>(int $seed): T {
  return T::build($seed);
}

Calling a method through a type parameter with no matching bound fails. Whim does not allow class-constant access through a type parameter.

Variance

Variance controls how one generic type relates to another.

Covariance

out T marks an output type parameter.

interface Source<out T> {
  public function read(): T;
}

If int fits int|string, then Source<int> fits Source<int|string>.

A covariant parameter may appear in return types and readonly properties. It may not appear where the caller can send a value in, such as a writable property or method parameter.

Contravariance

in T marks an input type parameter.

interface Sink<in T> {
  public function write(T $value): void;
}

If int fits mixed, then Sink<mixed> fits Sink<int>.

A contravariant parameter may appear in method parameters. It may not appear as an output type.

Invariance

An unmarked parameter is invariant. Cell<int> and Cell<int|string> are then different types, and neither fits the other only because their arguments do.

final class Cell<T> {
  public function __construct(public T $value) {}
}

Whim checks variance in aliases, classes, interfaces, functions, methods, closures, and arrows. A negation reverses the position while Whim checks it.

Generic inheritance

A child binds its parent’s parameters in its extends or implements clause.

interface Source<out T> {
  public function read(): T;
}

abstract class Base<T> {
  public function __construct(protected T $value) {}
}

final class IntegerSource extends Base<int> implements Source<int> {
  public function read(): int {
    return $this->value;
  }
}

The object is an IntegerSource, a Base<int>, and a Source<int>.

Whim rejects a class that reaches the same generic interface with incompatible type arguments.

Generic first-class callables

A first-class callable may remain unbound:

function identity<T>(T $value): T {
  return $value;
}

$generic = identity(...);
$value = $generic::<int>(42);

Calling $generic(42) fails because T has no binding or default.

Bind the type argument while creating the callable when all calls should use one type:

function identity<T>(T $value): T {
  return $value;
}

$integers = identity::<int>(...);
assert!($integers(42) == 42);

A bound callable keeps that type and rejects another binding.

Empty types and covariance

never is useful for variant types. A value such as Result<int, never> can fit Result<int, string> when the error parameter is covariant: it cannot hold an error, so the wider error type is safe.

The same rule explains why an empty vec<never> fits vec<int>.

Symbols as Types

Every Whim symbol has a type meaning. A name in a type position does not always mean a class.

This rule lets types name specific constants, enum cases, functions, and methods as well as objects.

Classes and interfaces

A class name accepts instances of that class and its children.

class Message {}
final class Notice extends Message {}

function send(Message $message): void {}

send(new Message());
send(new Notice());

An interface name accepts objects that implement it. Generic arguments remain part of either type.

Enums and cases

An enum name accepts every case of that enum. An enum case name accepts only that case.

enum Colour {
  case Red;
  case Blue;
}

function paint(Colour $colour): void {}
function paint_red(Colour::Red $colour): void {}

Case types work in unions, bounds, casts, and match arms.

Constants

A constant name accepts values equal to that constant.

const OK = 200;
const NOT_FOUND = 404;

function success(OK $status): void {}

success(200);

The same rule applies to class-like constants:

final class Codes {
  public const int SUCCESS = 200;
}

function success(Codes::SUCCESS $status): void {}

Two constants with the same value denote the same set of values, even when they have different names.

This also gives a bare constant its expected meaning in a match:

const OK = 200;

function label(int $status): string {
  return match ($status) {
    OK => 'ok',
    $_ => 'other',
  };
}

The arm compares the subject with the constant’s value. It does not treat OK as an undeclared class.

Functions

A function name as a type accepts first-class callables made from that function.

function add(int $left, int $right): int {
  return $left + $right;
}

function run_add(add $callable): int {
  return $callable(20, 22);
}

$callable = add(...);
assert!(run_add($callable) == 42);

Another callable with the same signature does not fit add. A partial call of add does not fit it either. Use fn(...) when origin does not matter.

For a generic function, the bare name describes the whole function family. A type argument selects one binding:

function identity<T>(T $value): T {
  return $value;
}

$integer = identity::<int>(...);
assert!($integer is identity);
assert!($integer is identity<int>);
assert!(!($integer is identity<string>));

A non-generic function takes no type arguments in a type.

Methods

Type::method accepts first-class callables made from that method family.

interface Mapper<T> {
  public function map<U>(T $value, U $fallback): U;
}

final class IntegerMapper implements Mapper<int> {
  public function map<U>(int $value, U $fallback): U {
    return $fallback;
  }
}

function call_map(Mapper<int>::map<string> $callable): string {
  return $callable(42, 'value');
}

$map = new IntegerMapper()->map::<string>(...);
assert!(call_map($map) == 'value');

The receiver must belong to the named class or interface family with compatible type arguments. A method on an unrelated class does not fit merely because its signature matches. A partial method call does not fit.

Inherited static method callables may fit both the parent and child method families.

Aliases and newtypes

A type alias expands to its target type. A newtype keeps a distinct tag while also fitting its backing type.

type Identifier = int;
newtype UserId = int;

assert!(1 is Identifier);
assert!(UserId(1) is UserId);
assert!(UserId(1) is int);
assert!(!(1 is UserId));

See Aliases and Newtypes for the full rules.

One symbol name set

At namespace scope, classes, interfaces, enums, constants, functions, aliases, and newtypes share one name set. Code cannot declare two kinds under one name.

Within a class-like body, methods, constants, and enum cases share one name set. Properties have $ names and use a separate set.

This rule removes any guess from a type name. Once Whim resolves a symbol, its kind defines the type meaning above.

Type identifiers

When code needs one key for any type, use Whim\Type\id::<T>(). For a value’s runtime type, use Whim\Type\of($value). These functions avoid trying to turn a type into source text, which cannot preserve aliases, bindings, and loaded symbol identity without ambiguity.

Aliases and Newtypes

Type aliases and newtypes both name a backing type. An alias is transparent. A newtype adds a runtime tag.

Type aliases

Declare an alias with type:

type UserName = string&!'';
type Coordinate = (float, float);

function greet(UserName $name): string {
  return 'Hello, ' . $name;
}

UserName and string&!'' are the same type. A value gains no tag when it passes through the alias.

Aliases may refer to aliases declared later. They may also be recursive when the recursion passes through a collection or other value structure.

type Json = null|bool|int|float|string|vec<Json>|dict<string, Json>;
type Tree<T> = T|(Tree<T>, Tree<T>);

Whim checks recursive values without running forever on cycles.

Generic aliases

An alias may have parameters, bounds, defaults, and variance.

type Pair<A, B> = (A, B);
type Source<out T> = fn(): T;
type NonNull<T: !null = int> = T;

The alias expands with its supplied arguments. Runtime diagnostics may keep the alias name when that name makes the failed contract clearer.

An alias cannot name bare void.

Newtypes

Declare a newtype with newtype:

newtype UserId = int;
newtype PostId = int;

Construct it by calling its name:

newtype UserId = int;
newtype PostId = int;

$user = UserId(7);
$post = PostId(7);

The constructor checks the backing type and adds the tag. UserId(7) fits UserId and int. Plain 7 fits int but not UserId. PostId(7) does not fit UserId.

newtype UserId = int;

function load(UserId $id): void {}

load(UserId(7));

Newtypes use their backing value directly for normal operations:

newtype Count = int;

$total = Count(2) + Count(3);
assert!($total == 5);

Member access, indexing, calls, iteration, arithmetic, comparison, and string joining act on the backing value when that operation supports it.

Casting newtypes

$value as Newtype checks the backing type and applies the newtype tag. ?as returns the tagged value or null.

newtype UserId = int;

$id = 7 as UserId;
$missing = 'seven' ?as UserId;

assert!($id is UserId);
assert!($missing == null);

Casting a newtype to its backing type succeeds. Casting between two newtypes checks the new target’s backing type and applies the target tag.

Generic newtypes

Newtypes may have parameters, bounds, defaults, and variance.

newtype Identifier<T: int|string = int> = T;
newtype Producer<out T> = fn(): T;

$number = Identifier(42);
$name = Identifier::<string>('user');

The constructor uses ::<...> when you supply type arguments.

Layered newtypes

A newtype may back another newtype:

newtype Inner = int;
newtype Outer = Inner;

$value = Outer(Inner(5));
assert!($value is Outer);
assert!($value is Inner);
assert!($value is int);

Each tag remains part of the value’s type.

Mutable backing values

A newtype check always includes the current backing value. Mutation can make a tagged collection stop fitting its declared newtype.

newtype Numbers = vec<int>;

$numbers = Numbers(vec[1]);
$numbers[] = 'changed';

assert!(!($numbers is Numbers));

Whim does not freeze the backing vec. It checks the type again when code crosses a typed boundary.

Dict keys are different. A dict stores normalized scalar keys, so a newtype tag on an integer, string, or boolean key does not remain in the dict. Do not use a newtype as a dict key type when the returned dict must still satisfy that tag.

Attributes

Type aliases and newtypes may carry attributes that target their symbol kinds. Whim\Attribute\Attribute defines separate target bits for aliases and newtypes.

Choosing one

Use an alias when two spellings should accept the same values. Use a newtype when code must not mix two values only because their backing types match.

type Port = 1..=65535;
newtype UserId = int;

Port refines integers without adding identity. UserId marks which domain an integer belongs to.

Throwing and Catching

throw stops the current path with an object that implements Whim\Unwind\Throwable.

use Whim\Unwind\InvalidArgumentException;

function divide(int $left, int $right): float {
  if ($right == 0) {
    throw new InvalidArgumentException('division by zero');
  }

  return $left / $right;
}

Throwing a scalar or an object that does not implement Throwable raises TypeError instead.

Throwable data

Whim\Unwind\Error and Whim\Unwind\Exception are the two built-in roots. Both implement Throwable and expose:

  • getMessage(): string
  • getCode(): int
  • getFile(): string
  • getLine(): int
  • getTrace(): vec<TraceFrame>
  • getPrevious(): null|Throwable
  • toString(): string

Their constructor accepts a message, an integer code, and an optional previous throwable.

use Whim\Unwind\RuntimeException;

$cause = new RuntimeException('connection failed');
$error = new RuntimeException('request failed', 0, $cause);

Whim records the file, line, and trace when it creates the throwable. Throwing the same object later does not replace that trace.

A trace frame contains the function name, file, line, and argument values. A marker attribute may hide a sensitive parameter.

Errors and exceptions

Error reports a language, compiler, or runtime contract failure. Examples include TypeError, OutOfBoundsError, ReadonlyError, and UndefinedSymbolError.

Exception reports a failure that application code may handle. LogicException and RuntimeException are its two main groups.

Both use the same throw and catch rules. The split tells readers whether a failure points to a broken program rule or an expected outside condition.

try and catch

try must have at least one catch, else, or finally clause.

use Whim\Unwind\InvalidArgumentException;

try {
  throw new InvalidArgumentException('bad value');
} catch (InvalidArgumentException $error) {
  write_line!($error->getMessage());
}

Whim tests catch clauses in source order. A clause may omit its variable:

try {
  throw new Whim\Unwind\RuntimeException('failed');
} catch (Whim\Unwind\RuntimeException) {
  write_line!('failed');
}

A union catches more than one type:

use Whim\Unwind\RuntimeException;

final class TimeoutException extends RuntimeException {}
final class NetworkException extends RuntimeException {}

function work(): void {}

function recover(TimeoutException|NetworkException $error): void {}

try {
  work();
} catch (TimeoutException|NetworkException $error) {
  recover($error);
}

If no clause matches, Whim unwinds to an outer try. A throw from a catch body also moves outward; sibling catches do not see it.

Catch guards

Add if after a catch to test the matched value:

use Whim\Unwind\RuntimeException;

try {
  throw new RuntimeException('missing', 404);
} catch (RuntimeException $error) if ($error->getCode() == 404) {
  write_line!('not found');
} catch (RuntimeException $error) {
  write_line!('other failure');
}

Whim runs a guard only after its type matches. A false guard moves to the next clause. If the guard throws, that new throwable replaces the current one and moves outward.

else

A try else block runs only when the try body reaches its end without a throw, return, break, or continue.

$completed = false;

try {
  write_line!('work');
} else {
  $completed = true;
}

assert!($completed);

It does not run after a catch. A throw from else moves outward; sibling catches do not see it.

Variables assigned in the try body are available in else under the normal scope rules.

finally

finally runs after the try body and any matching catch or else. It also runs before return, break, or continue transfers control out of the protected code.

try {
  write_line!('work');
} finally {
  write_line!('cleanup');
}

If finally throws, its throwable replaces a pending return or throwable. Nested finally blocks each run once while control moves outward.

Whim checks a pending return value after it runs finally. Cleanup still runs when the return later fails its declared type.

exit! ends normal execution. It does not run enclosing catch, else, or finally clauses. Whim still runs shutdown destructors.

panic!('message') does the same, but first writes the message and current stack trace to standard error. It always exits with status 255. It is for a broken invariant, not an error a caller can handle.

Uncaught throwables

An uncaught throwable ends the program with exit code 255. Whim writes its class, message, code, source location, notes, and stack trace to standard error.

Set WHIM_FULL_TRACE=true when the normal trace hides frames marked as boundaries.

Catch narrowly

Catch the type that the current code can handle. Catching Throwable and then continuing can hide type errors, broken invariants, and resource leaks. Let an unknown failure keep its original trace.

Option and Result

Use null|T for simple absence when null cannot be a valid T.

function find_name(int $id): null|string {
  return match ($id) {
    1 => 'Ada',
    $_ => null,
  };
}

Use Option<T> when a present value may itself be null. Use Result<T, E> when failure is data that the caller should inspect.

Keep nullable parameters nullable. Requiring callers to allocate an option to pass no argument adds work and makes calls harder to read.

Option values

Whim\Option\Some<T> contains a value. Whim\Option\None contains no value.

use Whim\Option\None;
use Whim\Option\Some;

$present = new Some::<null>(null);
$absent = new None();

assert!($present->isSome());
assert!($absent->isNone());

Some<null> differs from None, which is the reason to use an option here.

Whim\Option\some($value) and Whim\Option\none() are short constructors.

Reading an option

isSome() and isNone() test the branch.

unwrap() returns the value from Some. It throws LogicException on None. Use it only after isSome() or another rule proves the branch.

unwrapOr($default) returns the value or the given default. unwrapOrElse($fallback) calls the fallback only for None.

use Whim\Option;

$value = Option\none()->unwrapOr::<int>(42);
assert!($value == 42);

Changing an option

MethodSome<T>None
map($f)Some($f($value))unchanged None
mapOr($default, $f)$f($value)$default
andThen($f)$f($value)unchanged None
orElse($f)unchanged Some$f()
filter($test)same Some or Noneunchanged None
inspect($f)calls $f, then returns itselfreturns itself
okOr($error)Ok($value)Err($error)

Callbacks for a branch that does not run are not called.

Result values

Whim\Result\Ok<T> contains a success value. Whim\Result\Err<E> contains an error value.

use Whim\Result\Err;
use Whim\Result\Ok;
use Whim\Result\Result;

function parse_switch(string $value): Result<bool, string> {
  return match ($value) {
    'on' => new Ok::<bool>(true),
    'off' => new Ok::<bool>(false),
    $_ => new Err::<string>('expected on or off'),
  };
}

The error type need not implement Throwable. It may be a string, enum, object, or any other type.

Reading a result

isOk() and isErr() test the branch.

unwrap() returns the success value and throws LogicException on Err. unwrapErr() returns the error and throws on Ok.

unwrapOr($default) returns the success value or the default. unwrapOrElse($fallback) calls the fallback with the error only for Err.

ok() returns Some($value) or None. err() returns Some($error) or None.

Changing a result

MethodOk<T>Err<E>
map($f)Ok($f($value))keeps the error
mapErr($f)keeps the valueErr($f($error))
mapOr($default, $f)$f($value)$default
andThen($f)$f($value)keeps the error
orElse($f)keeps the value$f($error)
inspect($f)calls $f, then returns itselfreturns itself
inspectErr($f)returns itselfcalls $f, then returns itself

The generic return types keep all branches. For example, andThen returns Result<U, E|F> when the old error is E and the callback may return F.

Capturing a throwable

Whim\Result\attempt::<T, E>($callback) runs a callback and returns Ok<T> on success. It catches only E and returns Err<E>. Any other throwable continues outward.

use Whim\Result;
use Whim\Unwind\RuntimeException;

$result = Result\attempt::<int, RuntimeException>(
  fn(): int => throw new RuntimeException('failed'),
);

assert!($result->isErr());

E must implement Throwable.

Must-use results

Option and result methods carry #[MustUse]. If code discards their result by mistake, Whim raises DiscardedResultError. Use the returned value, pass it on, or write discard!(...) when ignoring it is deliberate.

Resources and Cleanup

Whim frees an object when code removes its last strong reference. A class can use __destruct(): void for final cleanup. Code that needs a strict lifetime can use using or drop!.

Ordinary lifetime

An object stays alive while a local, property, collection, closure, call frame, or trace holds it. Assigning over the last local reference may run its destructor at that statement.

final class Lease {
  public function __destruct(): void {
    write_line!('closed');
  }
}

$lease = new Lease();
$lease = null; // may print closed here

Arguments are strong references for the length of the call. Returning an object passes a strong reference to the caller.

using

using binds one or more values to a block and requires the block to hold the last strong reference to each value when it ends.

use Whim\IO\MemoryHandle;

using ($handle = new MemoryHandle()) {
  $handle->writeAll('temporary data');
}

The check runs on normal fallthrough, return, break, continue, and throw. If another strong reference still holds the value, Whim throws LeakedResourceError.

$escaped = null;

using ($handle = new Whim\IO\MemoryHandle()) {
  $escaped = $handle;
} // throws: $escaped still holds the handle

When a throw was already leaving the block, the leak error keeps that throwable as its previous error.

The binding is local to the using block. If an outer local has the same name, Whim restores it after the block. Reassigning the bound local does not change which original value the block owns.

Destructuring in using

A binding may use tuple, vec, or dict destructuring.

using (
  ($reader, $writer) = open_pair(),
) {
  copy($reader, $writer);
}

Whim checks each bound value as the block ends. Every value must have no other strong owner.

drop!

drop!($local) requires the local to hold the last strong reference, releases it, and makes the local undefined.

$resource = open_resource();
drop!($resource);

If another strong reference exists, Whim throws LeakedResourceError and keeps the local unchanged.

drop! accepts several locals. It checks all of them before dropping any, so a failure leaves every local intact.

drop!($first, $second, $third);

A self-cycle or another object cycle counts as another strong reference.

finally

Use finally when cleanup is an action rather than ownership of one value.

function guarded(bool $fail): void {
  try {
    if ($fail) {
      throw new Whim\Unwind\RuntimeException('failed');
    }
  } finally {
    write_line!('finished');
  }
}

guarded(false);

using checks ownership. finally always runs an action. Choose the rule the operation needs.

Explicit close methods

I/O handles and other closeable objects provide close(). Call it when code must handle a close error or release an outside resource before the object’s last reference dies.

The destructor remains a fallback. A safe close method should allow a second call, and its destructor should not mask an earlier failure.

Cycles

Strong references can form an unreachable cycle. Whim’s cycle collector finds such cycles and runs their destructors.

Whim\GC\collect_cycles() requests a collection and returns the number of boxes it freed. The runtime also starts collection after its cycle threshold. Set that threshold under [runtime] in whim.toml, or set WHIM_CYCLE_THRESHOLD for one run.

Do not depend on the exact order in which unrelated cycle destructors run.

References and Cycles

Object variables are strong references. They keep objects alive and preserve identity across assignments, calls, collections, properties, and closure captures.

The Whim\Reference namespace also provides weak references and weak maps.

Weak references

Weak<T> points to an object without keeping it alive.

use Whim\Reference\Weak;

final class User {
  public function __construct(public string $name) {}
}

function watch_user(): Weak<User> {
  $user = new User('Ada');
  $weak = new Weak::<User>($user);
  assert!($weak->get() is User);
  return $weak;
}

$weak = watch_user();
assert!($weak->get() == null);

get() returns null|T. A non-null result is a new strong reference for as long as the caller keeps it.

A weak reference does not stop using or drop! from releasing a value.

Weak maps

WeakMap<K, V> stores values under object keys without keeping those keys alive.

use Whim\Reference\WeakMap;

final class Request {}

$map = new WeakMap::<Request, string>();
$request = new Request();
$map->set($request, 'state');

assert!($map->has($request));
assert!($map->get($request) == 'state');

When code removes the last strong reference to a key, its entry leaves the map.

The main methods are:

  • set($key, $value): void
  • has($key): bool
  • get($key): V
  • remove($key): V
  • length(): int

get and remove throw OutOfBoundsError when the key has no entry.

Use a weak map for caches or per-object data that must not own the key. Use a dict when keys are scalar or when the map should own all of its data.

Strong cycles

Two or more objects may keep each other alive after code removes all outside references. Whim records possible cycles and collects them later.

final class Node {
  public null|Node $next = null;
}

$left = new Node();
$right = new Node();
$left->next = $right;
$right->next = $left;
$left = null;
$right = null;

Whim\GC\collect_cycles();

Weak references can break ownership cycles when one direction only needs to observe the other object.

Iterators

foreach can read an array, an Iterator<K, V>, or a ToIterator<K, V>.

$letters = dict['a' => 1, 'b' => 2];
foreach ($letters as $letter => $position) {
  write_line!($letter . ': ' . $position);
}

The key is optional:

foreach (vec['Ada', 'Grace'] as $name) {
  write_line!($name);
}

Iterator

An iterator owns one position. Its next() method returns the next key and value, or null after the last item.

interface Iterator<out K, out V> {
  public function next(): null|(K, V);
}

Passing an iterator to a second foreach does not rewind it. The second loop starts at the iterator’s current position.

ToIterator

A value that can start a new pass implements ToIterator<K, V>:

interface ToIterator<out K, out V> {
  public function toIterator(): Iterator<K, V>;
}

foreach calls toIterator() at the start of each pass. The method should return a new iterator unless the type has a clear reason to share a position.

Writing an iterable value

ArrayIterator turns any Whim array into an iterator.

use Whim\Iterate\ArrayIterator;
use Whim\Iterate\Iterator;
use Whim\Iterate\ToIterator;

final class Names implements ToIterator<int, string> {
  public function __construct(private vec<string> $names) {}

  public function toIterator(): Iterator<int, string> {
    return new ArrayIterator::<int, string>($this->names);
  }
}

$names = new Names(vec['Ada', 'Grace']);
foreach ($names as $index => $name) {
  write_line!($index . ': ' . $name);
}

Iterable types

Whim\Refine\Iterable<K, V> is the union of:

  • Iterator<K, V>
  • ToIterator<K, V>
  • array<K, V>

Library functions use this type when they only need to read a sequence. Such a function accepts a tuple, vec, dict, iterator, or iterable object without first copying it.

An iterator may yield data once. Do not assume that an Iterable can start a second pass unless it is an array or implements ToIterator.

Iterator helpers

map, filter, take, drop, and join return lazy iterators. They read an entry only when their next() method needs one. map and filter keep the source keys. take does not read past its limit.

Their concrete types are public: MapIterator, FilterIterator, TakeIterator, DropIterator, and JoinIterator. Use the helper functions for short pipelines, or construct these types when an API needs a concrete adapter.

count, reduce, to_vec, and to_dict consume the rest of an iterable. to_dict keeps its keys; to_vec keeps only its values.

use Whim\Iterate;

$values = Iterate\take::<int, int>(
  Iterate\filter::<int, int>(
    Iterate\map::<int, int, int>(
      vec[1, 2, 3, 4],
      fn(int $value): int => $value * 2,
    ),
    fn(int $value): bool => $value > 4,
  ),
  1,
);

assert!(Iterate\to_vec::<int, int>($values) == vec[6]);

Functions in Whim\Vec and Whim\Dict return complete arrays instead.

Use a lazy iterator for a long stream or when the caller may stop early. Use a vec or dict when the caller needs random access, a count, or more than one pass.

Tasks and Futures

Whim runs tasks on one event loop. A task may pause while it waits for a timer, a socket, a channel, or another task. Other ready tasks run during that pause. This is concurrency, not parallel work on several CPU cores.

Starting a task

Async\spawn() schedules a callable and returns a Future<T>.

use Whim\Async;

$future = Async\spawn::<int>(fn(): int {
  return 21 * 2;
});

$answer = $future->await();
assert!($answer == 42);

The task starts on a later event-loop turn. await() pauses the current task until the future succeeds or throws. If the task throws, await() throws the same error.

Code must observe every future. Await it, attach a result handler, return it, or call ignore() when the result does not matter. Dropping an unobserved failed future may raise UnhandledAwaitableException.

Future operations

Future<T> extends Promise<T> and provides:

  • await($cancellation): T waits for its result.
  • map($success): Future<U> changes a success value.
  • then($success, $failure): Future<U> handles either result.
  • catch($failure): Future<T|U> recovers from a failure.
  • always($callback): Future<T> runs after either result.
  • ignore(): static marks the result as intentionally unobserved.

These methods return a new future, except await() and ignore(). They do not change the source future.

use Whim\Async;

$length = Async\spawn::<string>(fn(): string => 'whim')
  ->map::<int>(fn(string $value): int => length!($value));

assert!($length->await() == 4);

Deferred results

Deferred<T> owns the write side of a future. Its consumer receives only the Future<T> interface.

use Whim\Async\Deferred;

$deferred = new Deferred::<int>();
$future = $deferred->getFuture();
$deferred->complete(42);
assert!($future->await() == 42);

Call complete($value) for success or error($error) for failure. A deferred may complete only once.

Waiting for many tasks

Async\all() awaits every keyed future and returns a dict with the same keys. It waits for all futures even when one fails. It throws the sole error or a CompositeException that contains all errors.

Async\concurrently() starts each supplied callable, then applies all(). Async\series() calls each callable in order without spawning tasks.

Async\first() returns or throws the first completed result. Async\any() returns the first successful result and throws CompositeException if every future fails. Both require at least one future. Futures that lose remain observable; neither function consumes or ignores their values.

Yielding and timers

  • Async\later() pauses until the next event-loop turn.
  • Async\sleep($duration, $cancellation) pauses for a duration.
  • Async\drain() runs scheduled work until no work remains.

Use later() to let ready work run without adding a time delay. Use sleep() for a real delay. Both may suspend the current task.

Task-local values

TaskLocal<T> stores one value per task. A new task starts with no value, even when the task that starts it has set one. Each TaskLocal object has its own storage.

use Whim\Async;
use Whim\Async\TaskLocal;

$requestId = new TaskLocal::<string>();
$requestId->set('main');

$future = Async\spawn::<null|string>(fn(): null|string => $requestId->get());
assert!($future->await() == null);
assert!($requestId->get() == 'main');

$requestId->clear();
assert!($requestId->get() == null);

Top-level code has its own task-local storage. get() returns null until the current task calls set(), and again after clear().

TaskGroup

TaskGroup tracks tasks that return void.

use Whim\Async\TaskGroup;

$group = new TaskGroup();
$group->defer(fn(): void {
  write_line!('first');
});
$group->defer(fn(): void {
  write_line!('second');
});
$group->awaitAll();

awaitAll() waits for every tracked task. It then throws the sole error or a CompositeException with all failures. A cancellation token cancels the wait, not the tasks already in the group.

WaitGroup

WaitGroup tracks work completed elsewhere. Call add() before starting one unit of work, call done() once it finishes, and call wait() to pause until the count reaches zero. done() throws if the count is already zero.

getCount() returns the current count. A cancellation token cancels only the wait.

Semaphore and Sequence

Semaphore<Tin, Tout> limits how many calls to one operation may run at once. Its constructor takes a positive limit and the operation. waitFor($input) waits for a slot, runs the operation, and releases the slot in a finally block.

Sequence<Tin, Tout> is a semaphore with a limit of one. It runs calls in order. Both types can report pending work, wait for a free slot, and cancel pending calls with a supplied error.

Channels and Cancellation

A channel sends typed values between tasks. Channel\bounded() creates a fixed-size buffer. Channel\unbounded() creates a buffer with no fixed limit. Both return a sender and a receiver.

use Whim\Async;
use Whim\Channel;

($sender, $receiver) = Channel\bounded::<string>(4);

$producer = Async\spawn::<null>(fn(): null {
  $sender->send('one');
  $sender->send('two');
  $sender->close();
  return null;
});

assert!($receiver->receive() == 'one');
assert!($receiver->receive() == 'two');
$producer->await();

Sending

Sender<T> provides:

  • send($value, $cancellation) waits for buffer space.
  • trySend($value) returns at once and throws FullException when full.
  • waitUntilSendable($cancellation) waits for space without sending a value.

Sending through a closed channel throws ClosedException.

Receiving

Receiver<T> provides:

  • receive($cancellation): T waits for a value.
  • tryReceive(): T returns at once and throws EmptyException when empty.
  • waitUntilReceivable($cancellation) waits for a value without taking it.

A receiver can drain values buffered before the channel closed. Once the closed channel is empty, receive operations throw ClosedException.

The API throws instead of using null to mean “no value.” A channel may carry null when T permits it.

Channel state

The sender and receiver share these operations:

  • getCapacity(): null|NonNegativeInt returns null for an unbounded channel.
  • count(): NonNegativeInt returns the buffered item count.
  • isFull() and isEmpty() inspect the buffer.
  • close() ends further writes.
  • isClosed() reports whether the channel has closed.

Closing an already closed channel has no further effect.

Cancellation tokens

Long waits accept null|CancellationToken. A wait with null does not observe a token.

A cancellation token provides four operations:

interface CancellationToken {
  public function isCancellationRequested(): bool;
  public function throwIfCancellationRequested(): void;
  public function register(fn(): void $callback): int;
  public function unregister(int $id): void;
}

Code that starts a cancellable wait should first call throwIfCancellationRequested(), then register a callback, and unregister it in finally. Most users should pass a token to library operations instead of managing registrations themselves.

Signal cancellation

SignalCancellationToken starts uncancelled. Calling cancel() marks it cancelled, runs each registered callback, and makes later checks throw CancelledException. A second cancel() has no effect. The optional cause is stored under the cancellation error.

If one callback throws, cancel() rethrows it. If several callbacks throw, it throws CompositeException.

use Whim\Async\SignalCancellationToken;

$token = new SignalCancellationToken();
$token->cancel();
assert!($token->isCancellationRequested());

Linked cancellation

LinkedCancellationToken accepts one or more source tokens. It cancels when any source cancels. Destroying it removes its source registrations.

Timeouts

TimeoutCancellationToken cancels after a Time\Duration. It may also take a parent token. It cancels when either the timer ends or the parent cancels.

The timer arms only while code has registered a callback. Direct calls to isCancellationRequested() and throwIfCancellationRequested() still check the elapsed time.

Timeout cancellation throws CancelledException whose cause is a TimeoutException. Parent cancellation has no timeout cause.

Cancellation is cooperative. It wakes operations that accept the token. It does not stop arbitrary code or undo work that already finished.

Attributes

An attribute adds typed data to a declaration or parameter. Whim checks the attribute class, its target, its arguments, and whether it may repeat.

Defining an attribute

Mark a class with Whim\Attribute\Attribute:

use Whim\Attribute\Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
final readonly class Table {
  public function __construct(public string $name) {}
}

#[Table('users')]
final class User {}

Applying #[Table('users')] creates a Table value. The arguments follow the same count and type rules as a normal constructor call. Attribute arguments may not read local variables.

An attribute class may have no constructor. Apply it without parentheses:

use Whim\Attribute\Attribute;

#[Attribute(Attribute::TARGET_FUNCTION)]
final class Endpoint {}

#[Endpoint]
function home(): string {
  return '/';
}

Targets

Pass one or more target flags to #[Attribute]:

FlagTarget
TARGET_CLASSclass, interface, or enum
TARGET_FUNCTIONnamed function or closure
TARGET_METHODmethod
TARGET_PROPERTYproperty
TARGET_CLASS_CONSTANTclass-like constant or enum case
TARGET_PARAMETERfunction, method, or closure parameter
TARGET_TYPE_ALIAStype alias
TARGET_NEWTYPEnewtype
TARGET_CONSTANTnamespace constant
TARGET_SYMBOLany named symbol
TARGET_ALLevery supported target

Join flags with |:

use Whim\Attribute\Attribute;

#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
final class Timed {}

With no flag, an attribute accepts every target.

Repeated attributes

An attribute may appear once on one target unless its flags include IS_REPEATABLE.

use Whim\Attribute\Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final readonly class Tag {
  public function __construct(public string $name) {}
}

#[Tag('http')]
#[Tag('public')]
final class Handler {}

Each use creates its own attribute value. Source order stays intact.

Reading attributes

The Whim\Attribute functions read stored attributes:

  • has_attribute($object, $class) checks the object’s class.
  • get_attribute::<T>($object) gets class attributes of type T.
  • get_attributes($object) gets all attributes on the object’s class.
  • get_function_attributes($name) gets function attributes.
  • get_method_attributes($object, $method) gets method attributes.
  • get_property_attributes($object, $property) gets property attributes.
  • get_constant_attributes($object, $constant) gets class constant attributes.
  • get_enum_case_attributes($case) gets enum case attributes.
  • get_parameter_attributes($object, $method, $parameter) gets method parameter attributes by name or position.

The member functions throw InvalidArgumentException when the named member does not exist. They return an empty vec when the member exists but has no attributes.

Attributes are values, not comments. The compiler rejects a bad target, a bad argument, a missing attribute class, or an illegal repeat before the program runs.

Built-in Attributes

Whim supplies attributes for call contracts and error reports.

MustUse

#[MustUse] requires the caller to consume a return value. It accepts an optional note.

use Whim\Marker\MustUse;

#[MustUse('store or send the token')]
function make_token(): string {
  return 'token';
}

$token = make_token();

Calling make_token(); as a statement raises DiscardedResultError. Call discard!(make_token()) when discarding the value is intentional.

Whim rejects #[MustUse] on a callable that returns void or never, since such a call has no result to consume.

SensitiveParameter

#[SensitiveParameter] hides one argument in stack traces.

use Whim\Marker\SensitiveParameter;

function sign(string $message, #[SensitiveParameter] string $secret): string {
  return $message . ':' . $secret;
}

The call still receives the real value. Only diagnostic output gets a SensitiveParameterValue in its place. SensitiveParameterValue::getValue() returns the hidden value to code that already holds that wrapper.

Deprecated

#[Deprecated] marks a symbol or member as old. Its first argument is the version that marked it old. Its optional second argument explains what to use.

use Whim\Marker\Deprecated;

#[Deprecated('0.2.0', 'use encode()')]
function old_encode(string $value): string {
  return $value;
}

Using the symbol reports the version and note.

TrackCaller

#[TrackCaller] moves an explicit throw site through marked wrappers to the first outer call without the marker. This keeps a small public wrapper from hiding the user’s call site.

Use it on an API that checks arguments or forwards an error. Do not add it to a callable that cannot throw and does not call user code.

TraceBoundary

#[TraceBoundary] hides the marked frame and deeper implementation frames from normal stack traces. WHIM_FULL_TRACE=true whim app.whim shows them.

Use it at a library boundary where lower frames add no useful action for the caller. It does not catch, change, or suppress an error.

Compiler Markers

These marker attributes tell the compiler how it may treat a declaration. Most application code does not need them.

Inline control

  • #[AlwaysInline] asks the optimizer to inline a resolved function or method.
  • #[NeverInline] forbids inlining.
  • #[Cold] marks a rare function or method and keeps it out of line.

These markers do not change a program’s result. They may change its bytecode, speed, code size, and stack trace. Measure before adding them.

Frameless

#[Frameless] marks an eligible function or method that can run without a call frame. The callable must take no parameters and return a literal value. The compiler rejects other shapes.

Frameless calls cannot hold normal call state. Use the marker only for tiny constant accessors supplied by the standard library.

Inheritance checks

#[ConsistentConstructor] makes child constructors keep a signature that is compatible with the parent constructor.

#[ConsistentGenerics] makes inherited generic bounds stay equal through the class family.

Both markers belong on classes. They let code use a class family without finding a different construction or generic contract on one child.

Stub

#[Stub] gives source tools the signature of a symbol whose implementation already exists in the language core. The compiler checks that the real symbol exists and matches the stub, then skips the source body.

The optional reason says why the symbol is a stub. The optional issue records the tracked speed work for a temporary built-in implementation.

namespace Whim\_Private;

use Whim\Marker\Stub;

#[Stub]
function getmypid(): (1..) {}

Only the standard library should declare stubs. A normal package must provide the code it declares.

Autoloading

Whim asks one registered callback to load a missing symbol. The standard library turns that callback into an ordered list of Autoloader values.

An autoloader receives the requested SymbolKind and full name. It returns true when it handled the request and false when the next loader should try.

Direct symbol maps

Autoloader::withSymbolFile() maps one full symbol name to a file. withSymbolFiles() adds a dict of mappings. Both return a cloned loader and leave the old value unchanged.

use Whim\Autoload;
use Whim\Autoload\Autoloader;

$loader = new Autoloader()
  ->withSymbolFile('App\\User', directory!() . '/src/User.whim')
  ->withSymbolFile('App\\load_user', directory!() . '/src/functions.whim');

Autoload\register($loader);

When Whim requests a mapped name, the loader calls require_once! for its file. The file must declare the requested symbol.

A fallback loader

withFallback() sets a callable for names outside the direct map.

use Whim\Autoload;
use Whim\Autoload\Autoloader;
use Whim\Symbol\SymbolKind;

$loader = new Autoloader()->withFallback(
  fn(SymbolKind $kind, string $name): bool {
    if ($name != 'App\\User') {
      return false;
    }

    require_once!(directory!() . '/src/User.whim');
    return true;
  },
);

Autoload\register($loader);

The kind tells the loader whether Whim needs a class, interface, enum, function, constant, type alias, or newtype. One name cannot belong to two symbol kinds.

The loader list

Autoload\register() appends a loader. unregister() removes every equal entry and does nothing when no entry matches. get_autoloaders() returns the current list in call order.

Autoload\load_symbol($kind, $name) asks the same chain used by the engine and returns whether the symbol now exists.

A loader that returns true must define the requested symbol. If it does not, Whim throws UndefinedSymbolError. Syntax, type, link, and top-level errors from a loaded file pass to the caller.

Generated dependency loader

whim install writes vendor/autoload.whim. Requiring it registers one loader for the root project and installed packages:

require_once!(directory!() . '/vendor/autoload.whim');

The generated loader contains fixed namespace maps. It does no network access, manifest parsing, package resolution, or directory scan. whim run does not look for it; the application must require it.

Git Dependencies

Whim uses Git repositories as package identities and SemVer tags as releases. It has no registry, package names, global cache, install scripts, features, or path-only package form.

Every package command stores data under the current project. whim run knows nothing about this system.

Whim also maintains official packages in separate Git repositories. They use the same commands and file layout as any other package.

Manifest

The root whim.toml may contain:

manifest-version = 1

[package]
repository = "https://github.com/acme/application"
homepage = "https://acme.example"
author = "Acme"
description = "An example application."
license = "MIT"
sponsor = "https://github.com/sponsors/acme"

[requirements]
whim = "^0.1"

[autoload.namespaces]
"App\\" = "src/"

[dependencies]
"git+https://github.com/acme/router.git" = "^1.2"

[dev-dependencies]
"git+ssh://[email protected]/acme/testing.git" = { version = "~2.0" }

[conflicts]
"git+https://github.com/acme/old-router.git" = "*"

[suggests]
"git+https://github.com/acme/profiler.git" = "^1"

[overrides]
"git+https://github.com/acme/router.git" = "git+https://github.com/acme/router-fork.git"

[format]
include = ["**/*.whim"]
exclude = ["src/generated/**"]
print_width = 80
tab_width = 2
use_tabs = false
end_of_line = "lf"

[runtime]
optimizations = "on"
call-depth = 10000
cycle-threshold = 10001
full-trace = false

Every manifest starts with manifest-version = 1. Unknown fields are errors.

Runtime settings

[runtime] controls program execution. optimizations accepts "on" or "off". call-depth sets the frame limit. cycle-threshold sets the cycle collector’s root limit. full-trace keeps TraceBoundary frames in traces.

All four fields are optional. WHIM_OPTIMIZATIONS, WHIM_CALL_DEPTH, WHIM_CYCLE_THRESHOLD, and WHIM_FULL_TRACE override them for one command. The boolean environment value accepts true or false.

Package details

[package] is optional. repository, homepage, author, description, license, and sponsor describe the project. They do not identify it or alter resolution.

license must be a valid SPDX expression. With no license, Whim treats the package as proprietary when it warns about incompatible licenses. sponsor must be an HTTP or HTTPS URL without credentials.

Whim warns when a dependency has no license. It also warns when a dependency requires a copyleft license but the root license has no copyleft choice. This check is a warning, not legal advice or a full license proof.

Engine requirement

[requirements].whim limits the Whim versions that may use the package. A missing requirement accepts any version. The root and every selected package must accept the running Whim version.

Source groups

[dependencies] is part of the running application. [dev-dependencies] is for development. One source cannot appear in both. Whim ignores development dependencies declared by installed packages.

Each value is a Cargo-style SemVer requirement. The short and table forms are equal:

[dependencies]
"git+https://github.com/acme/a.git" = "^1.2"
"git+https://github.com/acme/b.git" = { version = ">=2, <3" }

Whim accepts exact, caret, tilde, wildcard, and comma-joined comparisons. It does not accept ||. The requirement must permit a pre-release before the resolver can select it.

[conflicts] names versions that cannot share a graph with this package. [suggests] lists useful packages that Whim does not install on its own.

[overrides] belongs only in the root manifest. It tells Whim to read releases and code from a replacement repository while keeping the original repository as the graph identity. The replacement must have tags that satisfy the original requirements.

Namespace maps

Each [autoload.namespaces] key ends in \. Its value is a relative directory inside the repository. The empty prefix and Whim\ are forbidden. When prefixes overlap, the longest prefix wins. Two selected packages may not export the same exact prefix.

Git sources

Whim accepts:

  • git+https://
  • git+ssh://
  • git+file://
  • normal HTTPS URLs and SCP-style SSH input accepted by package commands

The manager normalizes command input before storing identity. It rejects plain HTTP, git://, relative paths, URL queries, fragments, passwords, tokens, and Git transport helpers. HTTPS, SSH, and local file URLs remain different identities even when they point to the same repository.

Each release tag must be 1.2.3 or v1.2.3. If both names point to one commit, they are one release. If they point to different commits, that version is ambiguous and cannot resolve. Whim ignores non-SemVer tags.

Resolution

Whim selects one version for each normalized source. All paths to that source must accept the same version. An override changes the Git repository that supplies versions and code; it does not change the source key in the graph.

The resolver prefers a version already present in whim.lock when that version still meets all rules. For an unlocked source, it chooses the highest matching release. It selects a pre-release only when a requirement permits that exact pre-release line.

Each selected manifest must accept the running Whim version. Whim rejects a self-dependency, a dependency cycle, a selected conflict, or two constraints with no common release. The error shows the sources and requirements that led to the conflict.

The resolver reads only runtime dependencies from installed packages. It also reads their Whim requirement, namespace maps, conflicts, suggestions, license, and sponsor link. It rejects overrides in an installed package.

Commands

init

whim init creates a project in the current directory. It writes whim.toml, src/main.whim, and tests/main.whim, then starts a Git repository unless the directory already belongs to one. It also writes .gitignore and .gitattributes rules for dependency and archive files.

whim init
whim init --no-git

--no-git skips Git and both Git files. Initialization preserves existing source files and existing Git rules. It never overwrites an existing manifest.

add and remove

whim add https://github.com/acme/router.git --version '^1.2'
whim add [email protected]:acme/testing.git --dev
whim remove https://github.com/acme/router.git

Without --version, add chooses the latest stable release and writes a caret requirement. Adding an existing source changes its requirement. Moving it between runtime and development requires removing it first.

Both commands resolve and stage the whole new state before replacing tracked files.

install

whim install
whim install --no-dev

With no lock, install resolves the graph and creates one. With a current lock, it installs those exact commits and does not choose newer tags. It fails when resolution fields in the manifest no longer match the lock.

--no-dev leaves the development closure and its namespaces out of the vendor tree and loader. A warm install reuses checked local data and need not contact the remote.

update

whim update
whim update https://github.com/acme/router.git

With no source, update may move the whole graph within its requirements. With sources, it unlocks those identities and moves other packages only when their constraints require it. A targeted update needs an existing lock.

If a locked version tag now points to another commit, update stops and reports the old and new commit IDs.

explain and inspect

whim show SOURCE prints details about one installed package. The report includes its locked version, tag, commit, install path, package metadata, namespace maps, Whim requirement, dependencies, development dependencies, conflicts, and suggestions. The source may use any spelling that normalizes to the locked Git identity.

whim why SOURCE prints the chain that requires a locked source and says whether it is installed.

whim why-not SOURCE --version RANGE explains why that source and range cannot join the current graph. The range defaults to *.

whim suggestions lists suggestions from the root project and installed packages. Each entry names who suggested it. whim fund prints Whim’s sponsor link and the sponsor links in the installed graph.

Lock and vendor tree

Commit whim.lock. It pins each normalized source, version, tag, commit, tree, manifest hash, exported-file hash, dependencies, optional replacement source, license, sponsor link, and suggestions. It stores no time, credential, or incidental working-directory path. A configured git+file:// source is an absolute identity, so its path appears in the lock.

The lock also stores a hash of the root fields that affect resolution:

  • the Whim requirement;
  • namespace maps;
  • runtime and development dependencies;
  • conflicts;
  • overrides.

Package details, format settings, suggestions, comments, and TOML order do not make a root lock stale. For an installed package, the hash covers its Whim requirement, namespace maps, runtime dependencies, and conflicts.

The lock parser rejects unknown fields, unsorted or repeated package entries, malformed hashes, and references to missing packages.

Ignore vendor/ unless the project wants to commit installed source for an offline release. Its layout is:

vendor/
  autoload.whim
  packages/<source-hash>/
  .whim/git/<source-hash>.git/
  .whim/stages/
  .whim/state.toml
  .whim/install.lock

Package directories use the full BLAKE3 hash of the normalized source. The local bare Git repositories live only in this project. install.lock stops two package commands from changing the project at once.

Loading packages

Applications opt in:

require_once!(directory!() . '/vendor/autoload.whim');

For App\Model\User mapped to src/, the loader tries:

  1. src/Model/User.whim
  2. a group file in src/Model/

The group file is classes.whim, interfaces.whim, enums.whim, functions.whim, types.whim, or constants.whim, based on the requested symbol kind.

The loader performs at most two file checks for one matched prefix. A file that loads but does not define the requested symbol causes UndefinedSymbolError.

Safety and transactions

Whim uses the system Git program without a shell. It works only with bare repositories that it created. It does not run package hooks, scripts, filters, submodules, or executables.

Remote Git commands stop after five minutes. Set WHIM_PACKAGE_NETWORK_TIMEOUT to a positive number of seconds to change this limit for one command.

The archive reader rejects absolute paths, parent traversal, links, devices, FIFOs, excessive path lengths, and size limits. Whim hashes sorted file paths, modes, lengths, and contents, then checks an existing vendor tree before reuse.

The fixed input limits are:

  • 1 MiB for a manifest, lock, or installed-state file;
  • 8,192 sources in one graph;
  • 100,000 tags from one source;
  • 100,000 archive entries, including directories, in one package;
  • 4,096 bytes in one package path;
  • 256 MiB in one file;
  • 1 GiB of unpacked data in one package.

Whim rejects archive links, devices, and FIFOs rather than trying to copy them. It keeps regular files, directories, and executable mode bits.

Package commands take a project lock. They prepare packages and the loader under vendor/.whim/stages/, and keep backups and recovery data under vendor/.whim/. They then swap the manifest, lock, vendor tree, and loader together. The project root changes only during that final swap. The next package command stops if it finds vendor/.whim/transaction.pending.

Inspect the manifest, lockfile, vendor tree, loader, state file, staging directories, and backups before removing that marker. Whim does not recover an interrupted swap on its own.

Official Packages

The Whim project maintains packages in the Trifle group on Codeberg. They live outside the main Whim repository and do not ship with the standard library.

These packages use names under Trifle\. Their Git tags define their versions. Install them through Whim’s Git package manager.

For example, this command adds the command-line argument parser:

whim add git+ssh://[email protected]/trifle/args

The group includes:

The Trifle group lists all official packages.

After you add Trifle\Diff and load vendor/autoload.whim, you can compare two sequences:

use Trifle\Diff;
use Trifle\Diff\Operation;

$edits = Diff\diff::<string>(vec['a', 'b', 'c'], vec['a', 'c', 'd']);

foreach ($edits as $edit) {
  $marker = match ($edit->operation) {
    Operation::Keep => ' ',
    Operation::Delete => '-',
    Operation::Insert => '+',
  };

  write_line!($marker . ' ' . $edit->value);
}

See Git Dependencies for manifests, locks, updates, and autoloading.

Debugging and Tests

Whim has language constructs for checks and debug output.

assert!

assert!($condition) requires a boolean. It throws AssertionError when the value is false.

$answer = 6 * 7;
assert!($answer == 42);

The error shows the source expression. A comparison also shows its left and right values.

Assertions always run. Whim has no release mode that removes them.

debug!

debug!($value) prints the call site and a detailed form of the value.

$numbers = vec[1, 2, 3];
debug!($numbers);

Strings show their byte length and escaped bytes. Objects show their class, generic arguments, visible property values, and hidden private values. Arrays show their size and items.

Debug output stops after 64 collection items and 32 nested levels. This keeps a large or cyclic value from filling the terminal. The path is relative to the working directory when possible.

Do not parse debug output. Its form may change between Whim releases.

Writing tests

Use assert! for each condition:

assert!(2 + 2 == 4);
assert!(length!('whim') == 4);
assert!(Whim\Str\contains('whim', 'him'));
assert!(contains_key!(vec[1, 2], 1));

You may add a message after the condition:

$status = 200;
assert!($status == 200, 'the request must succeed');

Whim does not impose a test file layout or test runner. A test is a normal Whim program that throws on failure. A shell, build tool, or Whim script can run a set of such files.

Stack traces

An uncaught throwable prints its type, message, source, and call stack. Normal traces stop at TraceBoundary frames. Set WHIM_FULL_TRACE=true when debugging the standard library itself:

WHIM_FULL_TRACE=true whim tests/example.whim

Parameters marked SensitiveParameter remain hidden in either trace mode.

panic!('message') prints the same kind of stack trace, then exits with status 255. It takes a literal string and cannot be caught. Use it to mark a state that must never occur.

Bytecode

Use disassembly when a result differs between optimized and unoptimized code, or when checking whether the optimizer found a known type:

whim disassemble example.whim > optimized.txt
WHIM_OPTIMIZATIONS=off whim disassemble example.whim > plain.txt

Bytecode is an implementation detail. It may change in any release.

Standard Library Rules

The standard library uses the Whim\ namespace. The CLI loads its compiled artifact before it compiles the entry file. Programs do not need to require it.

Whim also maintains official packages under the Trifle\ namespace. They do not ship in the standard-library artifact.

Public and private code

Public declarations live under domain namespaces such as Whim\Str, Whim\HTTP, and Whim\Database.

The language core supplies names under Whim\_Private. A domain _Private namespace holds Whim code used by that domain. Both forms are implementation details. Application code must not call them.

Most library code uses Whim. The core supplies operations that need the operating system, event loop, heap, or a tested low-level library.

Input and output types

Library functions take the broadest safe input and return the narrowest useful output. A read-only collection function normally accepts Refine\Iterable<K, V>, not only a vec or dict. A function that needs random access or a known size accepts an array.

Refined types state checked bounds in the signature:

use Whim\Refine\NonEmptyString;
use Whim\Refine\PositiveInt;

function repeat_name(NonEmptyString $name, PositiveInt $count): vec<string> {
  return Whim\Vec\fill($count, $name);
}

The engine checks those bounds at the call.

Missing values

The library uses null|T when T cannot itself contain null. It uses Option<T> when T is generic and may contain null, since Some(null) and None must stay different.

This rule keeps concrete APIs small without losing a value in generic code. Function parameters follow the same rule.

Errors

Library operations throw on failure. Result is available for APIs where an error is ordinary data, but it is not the default error path.

The Whim\Unwind hierarchy covers general argument, state, range, and runtime errors. A domain adds its own exception when callers need to distinguish that failure. Cancellation uses CancelledException.

Value style

Configuration and message objects are often readonly. Methods named with... return a changed clone:

$configuration = Whim\TCP\ListenConfiguration::default()
  ->withReuseAddress(true)
  ->withBacklog(256);

Mutable resources, pools, buffers, handles, and task controls use stateful methods. Any object that owns a closeable operating-system resource also closes it from its destructor. Use using for prompt cleanup.

Naming

Functions use snake_case; methods use camelCase. Types use PascalCase and constants use SCREAMING_SNAKE_CASE.

An operation driven by a callback often ends in _by. Its key-aware form often ends in _with_key or _by_key. A try... method returns at once and reports that it cannot proceed; a blocking form may suspend the current task.

Stability

Whim has no compatibility promise. The standard library may add, remove, rename, or redesign an API in any release. Read the docs that ship with the binary you run.

Core Types and Functions

This page covers small contracts used throughout the standard library.

Refine

Whim\Refine names common types:

  • ArrayKey is string|int|bool.
  • Numeric is int|float.
  • Scalar is int|float|string|bool.
  • Nullable<T> is T|null.
  • NonNull, NonEmptyString, NonEmptyVec<T>, and NonEmptyDict<K, V> exclude empty values.
  • PositiveInt, NonNegativeInt, NegativeInt, and NonPositiveInt name integer ranges.
  • Uint8, Int8, Uint16, Int16, Uint32, Int32, and Uint64 name fixed bounds.
  • Percent is 0..=100; Digit is 0..=9.
  • Iterable<K, V> accepts an iterator, a value that creates an iterator, or an array.
  • AnyTuple and AnyTupleOf<T> accept tuples of any supported length.
  • Exclude<T, N> removes N from T; Extract<T, U> keeps their overlap.

It also names common callables:

  • Predicate<T>: fn(T): bool
  • Transform<T, U>: fn(T): U
  • Reducer<T, U>: fn(U, T): U
  • Consumer<T>: fn(T): void
  • Supplier<T>: fn(): T
  • Comparator<T>: fn(T, T): Comparison\Ordering

Falsy and Truthy describe PHP’s old truth rules for data conversion. Whim conditions still require bool; Whim does not apply those aliases to if or loops.

Option and Result

Option<T> is the sealed family Some<T>|None. Some stores its public readonly value. None stores nothing.

The main operations are isSome, isNone, unwrap, unwrapOr, unwrapOrElse, map, mapOr, andThen, orElse, filter, okOr, and inspect. unwrap() on None throws LogicException.

Result<T, E> is the sealed family Ok<T>|Err<E>. Their public readonly fields are value and error. Result adds unwrapErr, mapErr, ok, err, and inspectErr to the same map and chain style. Unwrapping the wrong side throws LogicException.

Use Option\some($value) and Option\none() as short constructors. Result\attempt::<T, E>($callback) returns Ok or catches E into Err.

See Option and Result for examples and the null rule.

Comparison

Equal<T> defines equals(T): bool. Order<T> extends it with compare(T): Ordering and default methods for less, greater, min, max, and clamp.

Ordering has Less, Equal, and Greater. It can test each relation, reverse an order, or chain a second order through then and thenWith.

Collection sort functions take Comparator<T>, which returns Ordering, not an integer.

Conversion and defaults

Convert\ToString defines the explicit toString(): string contract. Whim has no magic __toString method.

Default\Default defines public static function default(): static. Types use it when one value is the clear default configuration or empty state.

Runtime type IDs

Whim\Type is part of the runtime core. Programs can use it without a standard-library artifact.

Type\of($value) returns the engine-local TypeId of a value. Type\id::<T>() returns the ID of a reified type. Equal types have equal IDs in one engine run.

Type\to_debug_string($id) returns a type spelling for logs and diagnostics. The spelling is not an identity or a source format. Two different IDs may have the same debug string, such as when one name denotes a type parameter in one place and a declared type in another. Never use the string as a map key or for type comparison; use TypeId itself. The spelling may change between Whim releases.

Type IDs are for maps and dispatch inside one process. They may change between runs. Do not store them in files or send them over a network.

Symbols and enums

Symbol\exists($name, $autoload) checks any named symbol. get_kind($name) returns its SymbolKind or throws when absent.

Every enum implements Enum\UnitEnum; a backed enum also implements BackedEnum<int|string>. Unit enums expose name; backed cases also expose value.

Garbage collection

Reference counts release ordinary values. GC\collect_cycles() asks the cycle collector to find unreachable strong cycles and returns the number it collected. Normal programs seldom need to call it.

Reflection

Whim\Reflection gives read-only access to loaded declarations, types, and values.

Find declarations

Each symbol kind has its own lookup function:

  • reflect_class, reflect_interface, and reflect_enum find class-like symbols.
  • reflect_type_alias and reflect_newtype find named types.
  • reflect_function and reflect_constant find functions and constants.
  • reflect_symbol finds any named symbol.
  • reflect_class_like accepts a class name or an object.

Each function returns null for a missing name or the wrong symbol kind. The second argument to reflect_symbol controls autoloading. Name-based lookup functions may invoke the registered autoloader.

namespace Example;

use Whim\Reflection;

final class User {
  public function __construct(public int $id) {}
}

$class = Reflection\reflect_class('Example\\User');
assert!($class != null);
assert!($class->getName() == 'Example\\User');
assert!($class->getProperty('id')?->isPromoted());

get_loaded_symbols() returns symbols loaded in the current engine. Its optional Whim\Symbol\SymbolKind argument filters the result. This function does not invoke an autoloader.

Origin and source

Each declaration has one DeclarationOrigin:

  • Core: Rust code in the engine.
  • Extension: code from a Whim artifact, including the standard library.
  • User: source that the program loads.

Only user declarations have a SourceLocation or a docblock. Core and extension declarations return null for both. A source location gives the file, byte offsets, lines, and columns. getDocumentation() returns the whole docblock, including /** and */.

Declarations list their attributes. getAttributes::<T>() returns attributes whose class fits T. getAttributesByName() matches an exact class name. AttributeReflection::newInstance() creates an instance of the attribute. No other reflection call constructs a user value.

Symbols and members

The Whim\Reflection\Symbol namespace has one reflection class for each named symbol kind.

On a class-like reflection, getMethods(), getProperties(), and getConstants() include inherited members. getDeclaredMethods(), getDeclaredProperties(), and getDeclaredConstants() return direct declarations only. A class reflection gives its parent, interfaces, constructor, destructor, flags, and attribute definition. An enum reflection gives its cases and backing type.

The Whim\Reflection\Member namespace covers methods, properties, class constants, and enum cases. Member lookup functions return null for a missing name. A method lists the parent or interface methods it implements. A property gives its type, default value, and promoted, readonly, and static flags.

Generics

A generic declaration lists its type parameters in source order. Each TypeParameterReflection gives its owner, position, variance, bounds, and default.

A TypeEnvironmentReflection maps each type parameter to its type argument. The declaration forms part of a type parameter’s identity, so two parameters named T use separate keys. Object and callable reflections include bindings from parent classes and interfaces.

getSpecialization() returns the type arguments that a class or object passes to a parent class or interface. TypeReflection::resolve() replaces type parameters with arguments from a type environment. Its second argument supplies the called class type for static.

Types

Three functions return type reflections:

  • reflect_type::<T>() reflects the reified type T.
  • reflect_type_of($value) reflects a value’s runtime type.
  • reflect_type_id($id) finds the type for an engine-local Whim\Type\TypeId.

TypeReflection reports the type kind, text, resolved state, and type ID. For a resolved type, accepts() tests a value, equals() compares types, and isSubtypeOf() tests the subtype relation.

Whim\Reflection\Type has a reflection class for each type form: primitive values, literals, integer ranges, named types, unions, intersections, negation, functions, collections, shapes, class names, tuples, wildcards, type parameters, and static.

toString() returns text for logs and error messages. The text is neither a type ID nor valid source code. getId() and equals() compare types.

Live values

ObjectReflection keeps a strong reference to its object and reports:

  • its class and reified class type;
  • its full type environment;
  • every instance property, including private properties from parent classes;
  • each property’s declared type, current value type, and whether it is initialized.

PropertyValueReflection::getValue() throws UninitializedPropertyError when the property is uninitialized.

reflect_callable() reports a callable’s declaration, function type, type bindings, bound object, called class, captured values, and bound arguments.

reflect_newtype_value() returns the outer newtype reflection, or null for a value that has no newtype. getBackingValue() returns the value inside that newtype. Pass the result to reflect_newtype_value() to inspect a nested newtype.

Strings, Numbers, and Binary Data

Byte strings

Whim strings hold bytes, so Whim\Str uses byte offsets and byte lengths.

Inspection functions include length, ord, chr, byte_at, compare, compare_ci, search, search_last, contains, starts_with, and ends_with. Search and containment functions accept a byte offset. The byte checks are is_whitespace, is_digit, is_letter, is_alphanumeric, is_hex_digit, and is_ascii_punctuation. A _ci suffix means ASCII case-insensitive matching.

Slice functions include slice, splice, chunk, split, range, before, after, and their last and case-insensitive forms.

Transform functions include ASCII lowercase, uppercase, capitalize, reverse, repeat, rot13, replacements, prefix and suffix stripping, padding, trimming, shuffling, word splitting, and wrapping.

use Whim\Str;

$words = Str\split('one,two,three', ',');
assert!(Str\join(' + ', $words) == 'one + two + three');
assert!(Str\starts_with('whimsical', 'whim'));
assert!(Str\slice('abcdef', 1, 3) == 'bcd');

Use Encoding\UTF8 before treating unknown bytes as Unicode text.

Unicode text and code points

Unicode\case_fold applies full, locale-independent case folding to valid UTF-8. It can expand one code point into several, such as \u{df} into ss. It throws EncodingException when the string is not valid UTF-8.

Unicode\code_point_at reads a scalar value at a byte offset. Unicode\code_point_before reads the value ending before an offset. They return null at the matching string end and U+FFFD for malformed UTF-8. Str\from_code_point encodes a Unicode\ScalarValue as UTF-8.

Unicode\CodePoint covers all code points from zero through U+10FFFF. Unicode\ScalarValue excludes the surrogate range, which UTF-8 cannot encode.

The other Whim\Unicode functions test integer code points without decoding a string. They check valid scalar values, whitespace, letters, marks, numbers, decimal digits, punctuation, symbols, separators, controls, and case. Every check returns false for an invalid code point.

use Whim\Unicode;

assert!(Unicode\case_fold("Stra\u{df}e") == 'strasse');
assert!(Unicode\code_point_at("\u{1f600}", 0) == 0x1f600);
assert!(Unicode\is_letter(0x4e2d));
assert!(Unicode\is_whitespace(0x3000));
assert!(Unicode\is_punctuation(0x3001));

Integers and floats

Int\try_parse($text) and Float\try_parse($text) return null for invalid input. They do not accept a partial number.

Float also tests NaN, finite, and infinite values. to_bits and from_bits convert a 64-bit float to its integer bit pattern. to_bytes and from_bytes use an explicit Binary\Endianness.

Math

Whim\Math provides checked integer division, absolute value, clamp, square root, exponent, logarithms, floor, ceiling, round, and trigonometry.

sum and sum_floats accept iterables. min, max, min_by, and max_by return null for no input. mean and median accept arrays because they need their size or more than one pass.

to_base, from_base, and base_convert support bases 2 through 36.

The namespace defines integer and float limits plus NAN, INF, E, and PI. Read each limit by its full name: positive minima and lowest signed values use different constants.

Ranges

Whim\Range represents full, lower-bound, upper-bound, and two-bound integer ranges. full, from, to, and between build them. Range objects expose their bounds and can create an iterator.

These objects are useful when a runtime value must carry a range. Type ranges such as 1..=10 remain part of the type system.

Binary encoding

Whim\Binary reads and writes signed and unsigned integers of 8, 16, 32, and 64 bits, plus 32-bit and 64-bit floats. Multi-byte functions require Endianness::Big or Endianness::Little.

One-shot encode_* functions return bytes; decode_* functions read bytes and check their exact width. MemoryReader, MemoryWriter, HandleReader, and HandleWriter provide moving cursors. Buffered readers report remaining data; buffered writers return their bytes through toString().

Use binary APIs for protocol fields and file formats. Do not reverse byte strings by hand.

Collections and Data Structures

Vec functions

Whim\Vec reads any Iterable<_, T> when keys do not matter and returns a packed vec.

Creation and access include values, keys, fill, reproduce, and integer range. Transform tools include map, map_with_key, map_nonnull, flat_map, enumerate, reductions, and reduce.

Filtering includes filter, key-aware filters, null removal, partitioning, and callback forms that keep non-null results.

Order and slice tools include reverse, unique, unique_by, sort, sort_by, shuffle, take, drop, slice, and chunk. equals compares vecs whose values implement Comparison\Equal<T>. concat, flatten, and zip combine inputs.

use Whim\Vec;

$values = Vec\map::<int, int>(vec[1, 2, 3], fn(int $n): int => $n * 2);
$even = Vec\filter::<int>($values, fn(int $n): bool => $n % 4 == 0);
assert!($even == vec[4]);

These functions return new vecs. Indexed assignment, append assignment, remove!, and swap_remove! change an existing vec variable. remove! preserves order; swap_remove! moves the last item into the removed index.

Dict functions

Whim\Dict keeps keys. It creates dicts from iterables, entries, keys, value selectors, groups, and counts.

Transform tools map values or keys, flatten nested keyed values, reindex, and flip. Filters preserve keys. Slice tools take, drop, pull, select keys, and apply while predicates.

merge, diff, and intersect have value and key forms. equal accepts a custom equality callback. Dict sorting can sort by value, selected value, or key while preserving keys.

Lazy iteration

Whim\Iterate holds Iterator, ToIterator, and ArrayIterator. Use it when the caller may stop early or the source is a stream. See Iterators.

Queue, stack, deque, and heap

Whim\DataStructure provides mutable focused containers:

  • Queue<T> adds at the back and removes from the front.
  • Stack<T> adds and removes at the top.
  • Deque<T> adds, removes, and peeks at both ends.
  • BinaryHeap<T> orders values with a comparator.
  • PriorityQueue<T, P> stores a value with a separate priority and comparator.

Empty removals and peeks return Option<T> so a stored null remains distinct from no item. Each type reports count, isEmpty, supports clear, converts to a vec, and implements ToIterator.

The heap and priority queue do not share an implementation. Equal values or priorities have no set relative order.

Time and Calendars

Whim\Time measures exact spans and clock readings. Whim\DateTime handles calendar dates, civil times, timezones, parsing, and formatting.

Duration

Time\Duration stores normalized seconds and nanoseconds. It can represent a positive, zero, or negative span.

Factory methods create spans from weeks, days, hours, minutes, seconds, milliseconds, microseconds, or nanoseconds. zero, second, millisecond, microsecond, nanosecond, and max return common values.

plus, minus, and invert return new durations. Total methods convert to minutes through nanoseconds. Float totals may lose precision; total nanoseconds returns an integer.

Monotonic and wall clocks

Time\Instant::now() reads a monotonic clock. Use it for elapsed work because wall-clock corrections do not move it backward. elapsed, durationSince, plus, and minus work with durations.

Time\SystemTime is a wall-clock reading. It supports now, unixEpoch, Unix timestamps, comparison, and duration arithmetic. Use it for dates, file times, protocol times, and stored event times.

Do not turn an Instant into a calendar date. Do not measure a timeout with SystemTime.

Date, Time, and DateTime

Date stores a year, month, and day. Time stores hour, minute, second, and nanosecond. DateTime combines them without a timezone.

Each type has a checked fromParts, a nullable parse, a throwing from, formatting, comparison, and a standard toString form. fromPartsUnsafe exists for trusted parsed data and does not replace input checks in application code.

use Whim\DateTime\Date;

$date = Date::from('2026-08-21');
assert!($date->toString() == '2026-08-21');
assert!($date->isLeapYear() == false);

Month, Weekday, Era, and Meridiem name calendar values. Refined aliases bound years, days, hours, minutes, seconds, nanoseconds, and UTC offsets.

Periods and durations

DateTime\Period stores calendar years, months, weeks, and days. Adding one month follows month length and an Overflow rule. Adding a Time\Duration adds exact elapsed time.

These differ around short months and daylight-saving changes. Use a period for “next month” or “tomorrow.” Use a duration for “after 3,600 seconds.”

TimeZone

TimeZone::from($id) loads an IANA name, fixed offset, or other accepted zone. utc() and system() return common zones. A zone can report its offset and abbreviation at a SystemTime, plus its prior and next transition.

Resolving a local DateTime may find no instant or two instants during a clock change. Disambiguation selects compatible, earlier, later, or reject behavior.

ZonedDateTime

ZonedDateTime joins one exact instant with a timezone and its local calendar fields. It can start from SystemTime, DateTime, current time, RFC 2822 text, or the standard parser.

withTimeZone keeps the instant and changes its displayed local fields. Duration arithmetic follows elapsed time. Period arithmetic follows the local calendar and resolves the result in the zone.

Formatting

DateTime\Formatter::fromPattern() builds a checked pattern. Ready formatters cover ISO date, ISO time, ISO date-time, RFC 3339, RFC 2822, Temporal text, and HTTP dates.

The formatter has separate methods for Date, Time, DateTime, and ZonedDateTime. Parsing methods return null on invalid text; from methods throw a domain error.

Encoding and Data Formats

Text and byte encodings

Whim\Encoding groups encodings by format:

  • Base32 supports its standard alphabets and optional padding.
  • Base64 supports standard and URL-safe alphabets and optional padding.
  • Hex encodes and decodes hexadecimal text.
  • URI percent-encodes full URI references without changing their syntax.
  • Url handles component and form encoding.
  • Punycode converts international labels.
  • QuotedPrintable handles text and binary modes plus strict or forgiving input.
  • EncodedWord handles encoded words used in mail headers.
  • UTF8\lossy replaces invalid UTF-8 with the replacement character.

Decode functions throw DecodingException on malformed input. Encode functions throw EncodingException when the format cannot hold a value.

These functions are not interchangeable. URI\encode preserves URI delimiters and valid percent escapes. URI\decode leaves escaped delimiters encoded, so decoding cannot turn data into URI syntax. URL form encoding maps spaces and plus signs by form rules; component encoding does not. Base64 URL-safe text uses a different alphabet from normal Base64.

JSON

Json\Value is:

null|bool|int|float|string|vec<Value>|dict<string, Value>

Json\encode($value, $pretty) accepts that union or an object implementing ToJson. decode($text) returns Json\Value. decode_as::<T>() calls T::fromJson() for a type that implements FromJson.

use Whim\Json;

$encoded = Json\encode(dict['ready' => true, 'count' => 3]);
$decoded = Json\decode($encoded);
assert!($decoded is dict<string, Json\Value>);

JSON objects have string keys. The encoder rejects non-finite floats. Bad text throws DecodingException; an unsupported value throws EncodingException.

CSV

CSV\Reader is an iterator of vec<string> records over an IO\ReadHandle. Its constructor sets the delimiter, enclosure, and escape byte. It reads as the caller asks for rows.

CSV\Writer writes records to an IO\WriteHandle. writeAll accepts an iterable of records. The CSV\read and CSV\write functions are short constructors.

Malformed quoting throws MalformedCSVException. CSV has no built-in schema; all fields are strings.

BSON

BSON\encode writes a BSON\Document to one byte string. BSON\decode reads one complete document. ToBson and FromBson map application values to and from documents.

The value union includes arrays and documents, 32-bit integers, binary data, object IDs, wall-clock times, regular expressions, timestamps, Decimal128, JavaScript, symbols, database pointers, and BSON marker values. ObjectId can parse, generate, compare, and render identifiers. Binary::fromUUID and toUUID convert UUID binary values.

BSON\Reader reads consecutive documents from an IO\ReadHandle. BSON\Writer writes them to an IO\WriteHandle. The reader rejects documents over 16 MiB by default; its constructor can set a lower or higher limit.

Bad bytes throw DecodingException. Values that BSON cannot represent throw EncodingException.

Compression

Compression\Codec creates a Compressor and Decompressor. Whim supplies:

  • Gzip
  • Deflate
  • Brotli
  • Zstandard

A transformer accepts chunks through push($bytes) and returns output now ready. finish() returns the last bytes. A compressor also has flush(). Finish a transformer once and do not push more input after it.

TransformReadHandle applies a transformer while reading another handle. This keeps the codec independent from files, sockets, and HTTP.

Registry maps lowercase content-coding names to codecs. It rejects duplicate names and provides contains, get, and codings. HTTP compression middleware uses the same registry.

HTML

HTML\escape_text escapes text-node content. escape_attribute applies the stricter attribute rules. decode and decode_attribute apply WHATWG character-reference rules for their matching contexts. entity expands one case-sensitive named reference written without & or ;, or returns null.

Escaping text does not make it safe as an unquoted attribute, URL, script, style, or HTTP header. Escape for the output context.

Regular expressions

Regex\Pattern::compile($source) compiles a byte regular expression or throws InvalidPatternException.

The pattern can test matches, find one MatchResult, replace all literal matches, or split a string. Regex\escape($literal) quotes bytes for a pattern.

MatchResult exposes its byte start, end, full value, byte length, and numbered or named captures. An absent or unmatched capture returns null.

Patterns work on bytes. Validate or repair UTF-8 first when an application needs Unicode text rules.

MIME values

MIME\MediaType parses a type, subtype, and parameters. essence() omits the parameters. Parameters and Headers are readonly ordered collections with case-insensitive names and original values.

ContentDisposition parses inline, attachment, names, and filenames. filename() returns a safe last path component; unsafeFilename() returns the raw declared value. ContentId parses and creates content IDs.

MIME\Sniff\from_string() detects a media type from a byte prefix. from_handle() samples a read handle without changing the caller’s logical content stream.

MIME parts and multipart data

A Part exposes mediaType(), headers(), and a streaming body() handle. Text, Data, and RawPart create common parts. Data transfer encoding runs as chunks rather than copying a whole attachment.

MultiPart owns a boundary and an ordered list of parts. Its body streams each boundary, header block, and part body. MultiPart\Parser reads a multipart body from a handle and spools large parts from memory to a temporary file.

Set the parser’s size, header, part, and spool limits for untrusted HTTP input. Bad boundaries or fields throw MultiPartException.

Files and I/O

Whim\IO uses small capability interfaces. A function asks only for the work it needs.

Handle contracts

  • Handle marks an I/O value.
  • ReadHandle reads bytes and waits for readable data.
  • WriteHandle writes bytes and waits for writable space.
  • BufferedReadHandle adds byte, line, and delimiter reads.
  • BufferedWriteHandle adds flush.
  • SeekHandle moves a cursor.
  • CloseHandle reports and closes owned state.
  • FileDescriptorHandle exposes an owned OS\FileDescriptor.

CloseHandle::__destruct() attempts a final close and discards its error. Use using when close time or errors matter.

Reading

tryRead($maxBytes) never waits. It returns bytes now ready and may return an empty string. waitUntilReadable($cancellation) suspends until a read may make progress. reachedEndOfDataSource() tells an empty read from the end.

read combines readiness and non-blocking reads. readAll reads through the end with an optional byte limit. readFixedSize requires an exact count.

Reader adds buffering around any ReadHandle. It can read one byte, one line, through a suffix, or through a suffix with a bound. Its private buffer preserves bytes read past a delimiter.

Writing

tryWrite($bytes) writes without waiting and returns the count. write waits as needed and may write part of the input. writeAll continues until it sends all bytes.

IO\copy moves all bytes from a read handle to a write handle. copy_chunked lets the caller choose a chunk size and limit. pipe runs a read-to-write copy as a task.

In-memory and adapted handles

MemoryHandle is readable, writable, seekable, closeable, and convertible to a string. Reads and writes share one cursor.

Other adapters include:

  • BoundedReadHandle fails after a read limit.
  • FixedLengthReadHandle exposes an exact length.
  • TruncatedReadHandle stops after a maximum length.
  • ConcatReadHandle reads several sources in order.
  • JoinedReadWriteHandle joins separate read and write sides.
  • TeeWriteHandle copies writes to several targets.
  • sink handles discard writes or expose end-of-input reads.
  • SpoolHandle keeps small data in memory and moves larger data to a temporary file while preserving one seekable handle.

Adapters do not claim an operating-system descriptor unless their own contract implements FileDescriptorHandle.

Standard handles

IO\input_handle(), write_handle(), and error_handle() return the process standard input, output, and error handles. They are descriptor-backed. The language write constructs use the same output channels.

Files

File\open_read_only, open_write_only, and open_read_write return typed file handles. WriteMode selects open-or-create, truncate, append, or must-create behavior.

use Whim\File;
use Whim\IO;

using ($source = File\open_read_only('input.txt')) {
  using ($target = File\open_write_only('output.txt')) {
    IO\copy($source, $target);
  }
}

File\read and File\write cover one-shot work. File handles expose their path and size, support seeking, and can take shared or exclusive locks. Writable file handles can change the file length with truncate and flush pending data and metadata with synchronize. Both operations run on the blocking worker pool and accept a cancellation token.

File system

Whim\Filesystem creates files, directories, hard links, symbolic links, named pipes, and temporary entries. It deletes, renames, copies, changes modes and owners, reads directories, reads links, and resolves canonical paths.

Inspection covers existence, node kind, access bits, metadata, and available or total disk space. metadata follows a symbolic link; symbolic_link_metadata inspects the link itself.

Callers must pass true to delete a directory tree. Permission values use octal literals such as 0o755.

Filesystem\exchange_creation_mask() replaces the process-wide file creation mask and returns its previous value. Set it during startup. A temporary change can race with file creation in another task or blocking worker.

Whim\Path\SEPARATOR is the POSIX path separator. Path strings use /.

File descriptors

OS\FileDescriptor owns one POSIX descriptor. duplicate($number) creates a new owned descriptor from an open number. toInt() returns its number. isClosed() and close() manage its lifetime.

Duplicating is not the same as borrowing an integer. The new object owns its descriptor and closes it.

Environment, Processes, and Terminals

Environment

Whim\Env reads the current process state:

  • get_arguments() returns arguments after the entry file.
  • get_variable() returns null for a missing environment value.
  • get_variables() returns all current environment values.
  • set_variable() and remove_variable() update the process environment.
  • current_directory() and set_current_directory() handle the working path.
  • home_directory() and temporary_directory() return common paths.
  • current_binary() returns the Whim executable path.
  • current_script() returns the entry source path when one exists.
use Whim\Env;

$arguments = Env\get_arguments();
$home = Env\home_directory();
if ($home != null) {
  write_line!($home);
}

Environment state belongs to the whole process. A library should prefer an explicit parameter when a value can vary by call.

Child commands

Command\Command is a readonly child-process definition. Start with Command::create($program), then add arguments, environment values, a working directory, stream choices, or a separate process group through with... methods.

A stream may use a pipe, inherit the parent’s stream, discard data, use a file descriptor, or use a terminal. The command API passes arguments directly; it does not join them into shell text.

start() returns a Child. run() waits and returns Output. output() returns standard output and throws FailedException on a bad exit. succeeds() returns a boolean.

A child exposes its process ID, running state, available pipe handles, signals, and waits. join() captures output. terminate() sends a graceful signal and then kills after its optional grace period. kill() stops at once.

ExitStatus is Exited|Signalled. Output stores status, standard output, and standard error.

Current process

Process\get_id() and get_parent_id() return process IDs. The identity functions read or change real, effective, and supplementary user and group IDs. Session and process-group functions inspect or change POSIX process relationships. Priority functions read or change a process’s scheduling priority. Resource-limit functions read or change the current process’s soft and hard limits; null means unlimited. A missing process argument means the current process.

Changing identities, groups, sessions, priorities, or resource limits may need operating-system permission. A failed operation throws RuntimeException with the system error as its cause.

Process\replace() replaces Whim with another program and returns never. Its argument list follows the program name. A null environment inherits the current one; a dict replaces it. Failure throws RuntimeException.

exists($pid) checks a process. signal and signal_group send a supported POSIX Signal.

cpu_times() returns the user and system CPU time consumed by the current process and its waited-for children. Each value is a Duration.

watch_signal() calls a function each time a catchable signal arrives. Keep the returned SignalWatcher alive while it is needed, then call close(). Its destructor also stops the watch.

find_executable($name) searches the current executable path and returns an absolute non-empty path or null.

Not every listed signal exists on every POSIX system. isSupported() checks the host; isCatchable() rejects signals such as forced kill and stop.

Host information

OS\information() identifies the running system and machine. uptime() returns a Duration. load_averages() reports the one-, five-, and fifteen-minute load averages. memory() reports total and available physical memory in bytes.

Users and groups

OS\find_user() and OS\find_group() query the operating-system account directory by name or numeric identifier. They return immutable User and Group records, or null when no record exists.

OS\groups_for_user() returns the available primary and supplementary group records for a user. A group’s memberNames contains only names explicitly listed in that group record; users whose primary group matches may be absent.

Account-directory lookups run on the blocking worker pool. They accept an optional cancellation token and do not block other Whim tasks.

Shell text

Use Shell\escape_argument() to quote one value for the POSIX shell and Shell\join() to quote and join a list. Use them only when the task needs shell syntax. Prefer Command for a normal program call.

Terminal

Terminal\attached($descriptor) checks whether a descriptor is a terminal. size($descriptor) returns columns and rows or null. path($descriptor) returns the terminal path or null.

Network, TLS, and Proxies

Network APIs use the event loop and accept cancellation tokens on waits. They return typed endpoints and capability interfaces rather than raw descriptor numbers.

IP addresses and CIDR

IP\Address stores either four IPv4 bytes or sixteen IPv6 bytes. parse returns null; from throws on bad text. v4 and v6 require one family. fromBytes accepts an exact 4-byte or 16-byte string.

An address can return canonical, expanded, byte, and reverse-DNS forms. It can test loopback, private, link-local, multicast, unspecified, documentation, and global unicast ranges. It can also test and create IPv4-mapped IPv6 addresses.

CIDR\Block joins an address and prefix. It reports membership, overlap, and the first and last address.

International domain names

IDNA\to_ascii converts a Unicode domain name to its ASCII form. IDNA\to_unicode converts an IDNA domain name to Unicode. Both apply UTS #46, strict host-name rules, and DNS length limits. Invalid input raises an encoding or decoding exception.

URI, IRI, and URL

URI\URI follows generic URI syntax. It may be relative. It stores optional scheme and authority plus path, query, and fragment. URI\resolve resolves a reference against a base.

IRI\IRI permits international text and converts to and from a URI. Its host handling applies IDNA through the standard conversion path.

URL\URL is the stricter form used by network clients. It requires a scheme and authority, uses a numeric port, exposes an Origin, and reads or builds query parameters. It converts to URI or IRI.

All three types have nullable parse, throwing from, readonly public parts, with... copies, normalization, equality, and toString.

Use URI for a relative reference, IRI for an international identifier, and URL for an address a client can connect to.

Endpoints and streams

Network\InternetEndpoint stores an IP\Address and port. UnixEndpoint stores a local socket path or null for an unnamed endpoint.

Network\Stream<TEndpoint> is a read, write, close, and file-descriptor handle. It reports local and peer endpoints and can shut down reads, writes, or both. Listener<TEndpoint> accepts streams.

An empty read is not enough to prove closure. Use the read-handle end check.

TCP

TCP\connect($host, $port, $configuration, $cancellation) resolves a name or uses an IP address and returns a TCP\Stream.

TCP\listen() binds an address and returns a listener. Listen configuration controls no-delay on accepted streams, address and port reuse, IPv6-only mode, and backlog. Connect configuration controls no-delay and an optional local bind.

DefaultConnector implements the connector interface for reusable clients. SecureConnector wraps another connector and performs TLS. Secure streams implement both the TCP and TLS stream contracts.

UDP

UDP\bind() returns a datagram socket. sendTo sends bytes and metadata to an endpoint. receive and tryReceive return a Datagram with bytes, sender, destination details, and congestion data when the host supplies it.

Connecting a UDP socket fixes its peer and returns ConnectedSocket; it does not create a byte stream. Connected sends still preserve datagram boundaries.

Bind configuration controls reuse, broadcast, IPv6-only mode, and socket buffer sizes.

Unix sockets

Unix\connect and Unix\listen use local socket paths. Unix\pair() returns two connected non-blocking streams. All expose file descriptors and work with the same I/O and cancellation APIs as TCP.

SOCKS

SOCKS\Connector implements TCP\ProxyConnector. Its configuration sets the proxy host and port plus optional username and password. It performs SOCKS5 negotiation, reports authentication and protocol errors, and then returns the same TCP stream interface.

TLS identities and settings

TLS\Certificate reads DER or PEM certificate data. Identity joins a certificate chain and private key.

Client configuration controls system roots, added roots, an optional client identity, ALPN values, TLS version bounds, peer verification, name checking, SNI, and session reuse.

Verification defaults to full checks. AllowSelfSigned supports local work; Disabled removes peer checks and should not protect a real remote connection.

Server configuration starts with an identity and can add SNI identities, client roots, optional or required client authentication, ALPN values, version bounds, and session reuse.

TLS over a stream

TLS\Connector<TEndpoint> wraps any matching network connector, not only TCP. Acceptor<TEndpoint> wraps an accepted stream. TLS\listen() wraps a listener.

A secure stream still exposes normal network reads and writes. Its ConnectionState reports the negotiated version, cipher suite, ALPN value, handshake kind, peer certificates, and server name where available.

TLS closes and errors remain separate from the transport’s own close and error types. Always close the secure stream, not only the stream under it.

HTTP Messages and Cookies

Whim\HTTP\Message holds transport-free HTTP values. Bodies use I/O handles, so a message need not hold all bytes in memory.

Fields

FieldMap is a readonly ordered list of (name, value) fields plus an index for case-insensitive lookup. from() validates field names and values.

  • get($name) returns the first value or null.
  • getAll($name) returns every value in order.
  • has($name) checks a name.
  • with replaces all values under a name.
  • withAdded appends one value.
  • without removes a name.
  • toVec, count, isEmpty, and toIterator inspect the map.

fromPartsUnsafe is for already parsed and indexed internal data. Application code should use from.

Request and response

Request stores method, request target, optional absolute URL, protocol version, fields, optional body, and optional future trailers. Build one with fromParts or fromURL. fromPartsUnsafe skips method and target checks.

Response stores status, protocol version, fields, optional body, and optional future trailers. Its constructor checks the refined status type at the call.

Both are readonly. withMethod, withStatus, withHeader, withBody, and other with... methods return changed copies.

ProtocolVersion names HTTP/1.0, HTTP/1.1, HTTP/2, and HTTP/3 message values. The included client and server support HTTP/1.1 and HTTP/2; the HTTP/3 enum case does not add an HTTP/3 transport.

Transaction joins informational responses with the final response. Exchange joins one request and one response. reason_phrase($status) returns a standard phrase when one exists.

Response helpers

Whim\HTTP\Message\Response provides:

  • json($value) with application/json
  • text($value) with UTF-8 plain text
  • html($value) with UTF-8 HTML
  • empty($status) with no body
  • redirect, see_other, temporary_redirect, and permanent_redirect

Text and HTML accept a string or Convert\ToString. JSON accepts Json\Value or Json\ToJson. Redirects accept a string, URI, or URL.

Request cookies

Cookie\Collection parses Cookie request fields into an ordered list of name-value pairs. Duplicate names stay available through getAll; get returns the first.

The collection implements iteration and toString. fromHeaders reads all cookie fields from a FieldMap.

Cookie\SetCookie stores name, value, expiry, max age, domain, path, secure, HTTP-only, and same-site settings. fromParts validates each part; parse returns null for a bad field. with... methods return changed copies.

Cookie\add($response, $cookie) appends one Set-Cookie field to a response. It does not replace other cookies.

SameSite has Strict, Lax, and None. A SameSite::None cookie should also be secure for current browsers.

Sessions

HTTP\Session\Session stores Json\Value entries under non-empty names. It supports contains, throwing get, set, remove, iteration, toDict, and clear. It can request a new identifier or destruction.

Session\Configuration sets the cookie template, idle timeout, optional cookie lifetime, and rolling expiry. The default cookie is __session, secure, HTTP-only, path /, and same-site lax. Pass a cookie with secure: false for plain HTTP local work.

MemoryStore keeps session records in one process. DatabaseStore uses the database contracts. Store writes use a revision check; a conflict throws ConflictException rather than dropping one concurrent update.

The server session middleware loads a record, attaches the session to the request context, and writes or deletes it after the handler. The context gives the handler getSession().

HTTP Client

The HTTP client sends HTTP\Message\Request values and returns a Transaction. It supports HTTP/1.1 and HTTP/2, connection reuse, TLS, proxies, middleware, redirects, retries, cookies, and cancellation.

Basic use

use Whim\HTTP\Client\DefaultClient;
use Whim\HTTP\Message\Request;
use Whim\URL\URL;

$client = new DefaultClient();
$request = Request::fromURL('GET', URL::from('https://example.com/'));
$transaction = $client->send($request);
$body = $transaction->response->body?->readAll();

Client::send() takes a request, per-send settings, and optional cancellation. The request needs an absolute URL or a base URL in the client settings.

The returned body is a read handle. Read or close it before expecting a pooled HTTP/1.1 connection to return to the pool.

DefaultClient

DefaultClient accepts a connector, a Configuration, and an iterable of middleware. The default connector pools direct TCP and TLS connections.

Configuration sets header and body size limits, informational response limits, a base URL, TLS settings, HTTP/2 settings, enabled protocol versions, and an optional proxy.

SendConfiguration overrides one call. It can also set callbacks for informational responses and connection metadata, plus a connection timeout.

The client rejects trailers without a body, CONNECT without a tunnel API, and a TRACE request with a body.

Connections and connectors

A Connector receives an origin, request, settings, and cancellation token. It returns a Connection. A connection reports protocol and endpoint metadata, whether the pool may reuse it, and an exchange operation.

DirectConnector opens one network connection. PooledConnector reuses safe connections by origin. UnixConnector sends HTTP over a Unix socket.

ProxyConfiguration supports an HTTP proxy URL, optional authorization, TLS to the proxy, a server-name override, and host bypass rules.

Middleware

Client middleware runs after the connector acquires a connection and before the protocol exchange. It receives the connection, request, effective settings, next handler, and cancellation token.

CookieJar stores accepted response cookies and adds matching cookies to later requests. It follows domain, path, secure, expiry, and same-site data available to the client. clear() removes stored cookies and count() reports them.

DeniedDestinationsMiddleware rejects configured IP blocks after resolution. publicOnly() blocks private, loopback, link-local, and other non-public destinations. Use it when a caller controls the target URL.

Redirects and retries

RedirectClient wraps any client. It follows 301, 302, 303, 307, and 308 with a fixed limit. It strips credentials on cross-origin moves, follows safe referrer rules, and rewinds a seekable request body when a redirect must replay it. A body that cannot rewind stops the redirect.

RetryClient retries idempotent methods after connection or transport errors. It uses bounded attempts and increasing delays. A request body must be seekable to replay.

Redirect and retry are client decorators, not connection middleware, because they may need another connection.

WebSocket client

HTTP\WebSocket\Client\connect() performs a WebSocket handshake for a URL and returns a client connection. Configuration sets frame and message limits, headers, origin, subprotocols, response header size, and TLS settings.

The connection adds URL, response fields, endpoints, and TLS state to the common WebSocket connection API described in the server chapter.

HTTP Server

The server accepts HTTP/1.1 and HTTP/2 streams, parses requests, calls one handler, and writes responses. Its public layer stays in Whim and works with the network and I/O interfaces.

Handler

Every request reaches:

interface Handler {
  public function handle(
    Context $context,
    Request $request,
    CancellationToken $cancellation,
  ): Response;
}

FunctionHandler accepts a callable with any useful subset of context, request, and cancellation parameters, or no parameters. It adapts that callable once in its constructor.

use Whim\HTTP\Server\Handler\FunctionHandler;

$handler = new FunctionHandler(
  fn(): Whim\HTTP\Message\Response =>
    Whim\HTTP\Message\Response\text('hello'),
);

Context

Every context has protocol, local endpoint, peer endpoint, and a mutable dict of string parameters. It can hold one session and register callbacks that run when the response completes.

Some contexts add a capability:

  • Informational can send a 1xx response.
  • Push can start an HTTP/2 server push.
  • Secure exposes TLS security state.
  • Upgrade can hand the stream to another protocol.

Check the interface with is before using an optional capability.

Bindings and server life

Binding\Stream wraps a network listener and an enableH2c choice. A TLS listener negotiates HTTP/2 through ALPN; a plain listener can opt into cleartext HTTP/2.

Server takes one or more bindings and a configuration. serve($handler, $cancellation) runs until cancellation, closed bindings, or a fatal accept or connection error. One server object can serve only once.

Shutdown stops accepts, lets active requests drain up to the shutdown timeout, then closes remaining connections.

Configuration bounds idle time, header and body time, response writes, connections, connections per peer, concurrent requests, server pushes, header and body sizes, requests per connection, HTTP/2 settings, middleware, and the error responder.

Middleware

Server middleware receives context, request, next handler, and cancellation. It may change the request, call the next handler, change the response, or return without calling next.

Middleware\wrap($handler, $layers) creates a handler chain. The server’s configuration can hold the same middleware list.

Built-in middleware includes:

  • CORS for origin, method, field, credential, and cache rules.
  • HandlerTimeout for an optional per-handler deadline.
  • RequestDecompression for registered content codings and size bounds.
  • ResponseCompression for Accept-Encoding negotiation.
  • Session for cookie-backed stored sessions.
  • FunctionMiddleware for a callable adapter.

Handler timeouts are not part of the base server. Add the middleware when the application wants that policy.

Errors and responders

HTTPException carries a 4xx or 5xx status for expected request failure. Other throwables become server errors. The configured Responder turns a status and optional cause into a response.

Bare returns an empty error response. Debug includes the throwable and full trace text and is suitable only for local work. A responder failure or an exception from an upgrade callback stops the server and remains visible.

Router

HTTP\Router\Router implements both Handler and RouteCollection. add registers one method, path pattern, and handler. Helpers cover GET, HEAD, POST, PUT, PATCH, DELETE, and OPTIONS.

Patterns support:

  • literal text;
  • {name} for one non-empty path segment;
  • {name:expression} for a segment checked by a byte regular expression;
  • [text] for an optional sequence;
  • a final * for the rest of the path, stored under parameter name *;
  • \* for a literal asterisk.

A match percent-decodes each captured value and appends it to Context::$parameters before it calls the route handler. Router\parameter($context, $name) returns one value or null.

prefix($path, $group) adds a prefix to every route registered in the group. through($middleware, $group) wraps only that group. The router implements ToIterator over (method, pattern, handler) registrations.

No path match throws NotFoundException. A path with no matching method throws MethodNotAllowedException and includes the allowed methods.

Static files

Handler\StaticFiles serves a root directory under a URL prefix. It supports an index file, fixed response fields, cache control, media sniffing, ranges, conditional requests, and safe path resolution. It rejects traversal outside the root.

The file body stays an I/O handle. The handler does not read the full file into memory.

WebSocket server

HTTP\WebSocket\Server\Handler upgrades an HTTP/1.1 route and gives a Connection to a ConnectionHandler. FunctionConnectionHandler adapts callables with useful subsets of its arguments.

A connection can send text, binary, ping, and close frames. receive() accepts a per-call cancellation token. tryReceive() returns null when no complete message is ready; waitUntilReceivable() waits without taking one.

Received messages are TextMessage, BinaryMessage, or CloseMessage. The connection reports its subprotocol and close code and reason. Configuration bounds frames and complete messages and lists accepted subprotocols.

Destroying a live connection attempts a safe close without letting a transport error escape the destructor.

Databases

Whim\Database defines one async contract for SQLite, PostgreSQL, custom drivers, and connection pools. It accepts raw parameterized SQL and does not include a query builder.

Values and rows

Database\Value is null|bool|int|float|string|Blob. Blob is a newtype over string, so text and binary parameters stay distinct. A Row is a vec of values.

Column stores result-column name and database type data.

Connectors and connections

A Connector opens a Connection with an optional cancellation token. A connection can:

  • execute parameterized SQL
  • prepare a statement
  • begin a transaction
  • check the server with ping
  • report whether it is safe to reuse
  • close

SQL parameters use $1, $2, and so on. The number controls binding; first appearance does not. Pass values in numeric placeholder order.

use Whim\Database;
use Whim\Database\PostgreSQL\Connector;

using ($connection = new Connector('dbname=app')->connect()) {
  using ($result = $connection->execute(
    'SELECT id, name FROM users WHERE id = $1',
    vec[42],
  )) {
    $row = $result->fetch();
  }
}

Prepared Statement::execute() accepts only its values and cancellation. Statements and results are closeable.

Results

A result exposes column metadata, an affected-row count when the operation has one, and streaming fetch(). Fetch returns one row or null at the end.

Helpers cover common shapes:

  • fetch_one returns one row or null and rejects extra rows.
  • fetch_all returns every row.
  • fetch_value returns one selected value.
  • transactional begins, calls a function, commits on success, and rolls back on failure.

Close a result after an early stop so its connection can run another operation.

Transactions

begin($isolation, $readOnly, $cancellation) starts a transaction. A null isolation uses the database default. TransactionIsolation has read uncommitted, read committed, repeatable read, and serializable levels, though a driver may reject a level its database cannot provide.

A transaction implements Executor and adds isActive, commit, rollback, and close. Closing an active transaction rolls it back.

Pool

ConnectionPool wraps a connector. connect() checks out a lease that still implements Connection; closing it returns a reusable connection to the pool.

PoolConfiguration controls:

  • maximum open connections
  • maximum idle connections
  • idle timeout
  • total connection lifetime
  • validation interval
  • acquisition timeout

The default acquisition timeout is 30 seconds. A null timeout waits without a pool deadline, though the caller’s cancellation token can still end the wait.

The pool reports open, idle, and waiting counts. Closing it closes idle connections and fails pending acquisitions. Checked-out leases close or return when their owners finish.

SQLite

Database\SQLite\Connector opens a path or URI. inMemory() creates a private in-memory database.

Configuration sets read-only and create behavior, URI parsing, busy timeout, statement-cache size, and foreign-key enforcement. SQLite work runs through the shared bounded blocking pool, so file work does not stop the event loop.

Each connection permits one active operation. The driver waits for its prior worker operation to retire before reuse; application code does not retry a private busy state.

PostgreSQL

Database\PostgreSQL\Connector accepts a libpq connection string, which is marked sensitive in traces. It uses non-blocking libpq socket progress with the Whim event loop.

PostgreSQL errors preserve SQLSTATE, detail, and hint on Database\Exception. Call getSQLState() instead of matching error text.

Errors

The hierarchy separates connection, query, and transaction failures. AcquisitionTimeoutException means a pool wait expired. ConcurrentOperationException means code attempted overlapping work on one connection. Database server “busy” or lock errors remain query errors with their database code.

Mail and MIME Messages

Whim\Message models Internet mail. Whim\MIME supplies its content parts and headers. Whim\SMTP sends a message and envelope.

Addresses

Message\Address\Mailbox stores local part, domain, and optional display name. Group stores a named address group. AddressList holds mailboxes and groups, supports iteration, and can flatten its mailboxes.

Each type has nullable parse, throwing from, and toString. Mailbox also has checked fromParts. Output constructors enforce valid dot-atom and domain shape instead of accepting header injection.

Message IDs

MessageId parses, creates, and formats a message identifier. generate() uses secure random data plus an optional domain. MIME\ContentId supplies the same shape for body parts and can form a cid: URI.

Message

Message\Message::create() starts an immutable message. withFrom, withSender, withTo, withCc, withBcc, withReplyTo, withDate, withMessageId, withSubject, withReferences, withInReplyTo, and withContent return changed messages and keep matching headers in sync.

withHeader and withoutHeader handle custom fields. fromHeaders parses the known structured fields from an existing MIME header map.

Message\parse($handle) reads a message and MIME body. serialize($message) returns a streaming read handle. Large attachments stay streaming through transfer encoding and multipart output.

Envelope

An SMTP envelope is separate from visible message headers. Envelope::fromParts takes an optional sender and at least one recipient. fromMessage derives the sender from Sender or the first From mailbox and recipients from To, Cc, and Bcc.

Bcc addresses belong in the envelope but should not appear in serialized visible headers.

SMTP values

SMTP\Command and Reply parse and format protocol lines. EnhancedStatusCode stores its class, subject, and detail. Enums name capabilities, security modes, priority, delivery status requests, return modes, and delivery deadlines.

Transport

SMTP\Client\DefaultTransport connects, negotiates EHLO features, applies optional authentication, sends a message, and pools idle connections.

Transport configuration sets host, optional port, plaintext, STARTTLS, or implicit TLS, local hostname, pipelining, chunking, chunk size, partial-success policy, idle pool limits, connector, and TLS settings.

When the caller omits a port, STARTTLS uses 587, implicit TLS uses 465, and plaintext uses 25.

Idle connections have a timeout. A checked-out idle connection must answer NOOP; otherwise the transport closes it and opens a new one.

Authentication

Authenticators implement one contract over an SMTP connection. The library includes PLAIN, LOGIN, CRAM-MD5, SCRAM-SHA-256, and XOAUTH2. Credentials carry SensitiveParameter markers.

The server must advertise the chosen method. A missing or rejected method throws AuthenticationException, not a missing dict-key error.

Delivery options and report

Per-send settings cover delivery status notifications, envelope ID, required TLS, priority, deliver-by, and future release when the server advertises those extensions.

With partial success disabled, a rejected recipient fails the send. With it enabled, DeliveryReport lists every rejected mailbox and reply, including the case where the server rejects all recipients.

Connection, authentication, protocol, extension, and transmission failures use separate exception types.

Hashes, Passwords, and Random Data

Hashes

Hash\Algorithm names SHA-2, SHA-3, Keccak-256, SHA-1, MD5, RIPEMD-160, BLAKE3, BLAKE2b-256, CRC, Adler, and xxHash forms. Each case reports its digest length and whether it is cryptographic.

Hash\digest($algorithm, $bytes, $hex) computes one digest. Hasher accepts chunks through update and returns bytes from one final finish.

Hash\hmac computes a keyed MAC for a supported digest. HmacHasher is its streaming form. Hash\equals compares secret byte strings in fixed time for their length.

Hash\pbkdf2_sha256($password, $salt, $iterations) derives 32 bytes with PBKDF2-HMAC-SHA-256. The iteration count must be between 1 and 1,000,000.

Do not use CRC, Adler, xxHash, MD5, or SHA-1 for a new security check. Their presence supports checksums and old formats.

Password hashes

Password\hash($password, $algorithm) creates a fresh salt and returns a self-describing hash. Algorithms are Bcrypt, Argon2i, and Argon2id, with checked cost settings.

verify checks a password and returns false for an unknown hash. needs_rehash reports whether a stored hash uses a different algorithm or cost.

Bcrypt accepts at most 72 password bytes; the function rejects longer input instead of truncating it. Traces hide passwords and stored hashes.

Secure random data

SecureRandom\bytes($length) reads operating-system random bytes. string($length, $alphabet) selects unbiased characters from an alphabet. int($min, $max) selects an inclusive unbiased integer. float() returns a value from zero inclusive to one exclusive.

The default alphabet for a random string is safe for common text uses. Supply an explicit alphabet when a protocol has a fixed one.

Failure to obtain enough operating-system randomness throws InsufficientEntropyException.

Pseudo-random sequences

PseudoRandom\int and float use one process sequence seeded from secure random bytes. They are suitable for tests, sampling, games, and shuffling, not for keys, tokens, salts, or passwords.

RandomSequence\MersenneTwisterSequence accepts an explicit 32-bit seed and is repeatable. SecureSequence reads fresh secure data. Both implement Sequence with next, nextFloat, and inclusive nextIn.

Use an explicit sequence object when a test needs the same output on every run.

UUID

UUID\UUID::v4() creates a random UUID. v7() creates a time-ordered UUID. parse accepts canonical lowercase text or returns null; from throws.

fromBytes requires exactly 16 bytes. toBytes, toString, version, and equals expose the value without changing it. Parsed UUIDs may have no known version.

Namespace Index

This index lists the public standard-library namespaces. Names that end in \_Private are not public and do not appear here.

Core values and contracts

NamespacePurpose
Whimthe VERSION constant
Whim\Attributeread attributes from symbols and members
Whim\Autoloadregister and run symbol autoloaders
Whim\Comparisonequality, order, and Ordering
Whim\Convertexplicit value conversion contracts
Whim\Defaultthe default-value contract
Whim\Enuminterfaces implemented by all enums
Whim\GCexplicit cycle collection
Whim\Markerbuilt-in attributes and compiler markers
Whim\OptionSome, None, and option helpers
Whim\Promisethe read-only async result contract
Whim\Referenceweak references and weak maps
Whim\Refinecommon aliases, ranges, and callable types
Whim\Reflectionread-only access to loaded declarations, types, and values
Whim\Reflection\Attributeattribute rules and target kinds
Whim\Reflection\Callablefunctions, methods, closures, captures, and bound arguments
Whim\Reflection\Generictype parameters, bindings, and type environments
Whim\Reflection\Membermethods, properties, constants, and enum cases
Whim\Reflection\Symbolclasses, interfaces, enums, aliases, newtypes, functions, and constants
Whim\Reflection\Typetype forms and their parts
Whim\ResultOk, Err, and throwable capture
Whim\Symbolsymbol lookup and symbol kinds
Whim\Typeengine-local type identifiers
Whim\Unwinderrors, exceptions, throwables, and trace frames

See Core Types and Functions, Reflection, Option and Result, and Built-in Attributes.

Strings, numbers, and collections

NamespacePurpose
Whim\Binaryfixed-width integer and float encoding
Whim\Dicteager keyed collection functions
Whim\Floatfloat parsing, bit forms, and checks
Whim\Intinteger parsing
Whim\Iterateiterators and lazy collection functions
Whim\Matharithmetic, statistics, bases, and math constants
Whim\Rangeruntime integer range objects
Whim\Strbyte-string search and changes
Whim\UnicodeUnicode case folding and code-point checks
Whim\Veceager list functions

See Strings, Numbers, and Binary Data, Collections and Data Structures, and Iterators.

Data structures and formats

NamespacePurpose
Whim\BSONBSON values, encoding, decoding, readers, and writers
Whim\CSVstreaming CSV readers and writers
Whim\Compressiongzip, deflate, Brotli, and Zstandard streams
Whim\DataStructurequeue, stack, deque, heap, and priority queue
Whim\Encodingshared encoding errors and contracts
Whim\Encoding\Base32Base32 text
Whim\Encoding\Base64standard and URL-safe Base64
Whim\Encoding\EncodedWordmail header encoded words
Whim\Encoding\Hexhexadecimal text
Whim\Encoding\PunycodePunycode labels
Whim\Encoding\QuotedPrintablequoted-printable text and bytes
Whim\Encoding\URIwhole-URI percent encoding
Whim\Encoding\UTF8UTF-8 checks and lossy repair
Whim\Encoding\Urlpercent and form encoding
Whim\HTMLWHATWG character references and escaping
Whim\JsonJSON values, encoding, and decoding
Whim\MIMEmedia types, fields, content IDs, and parts
Whim\MIME\MultiPartmultipart writing and parsing
Whim\MIME\Parttext, data, and raw MIME parts
Whim\MIME\Sniffmedia-type checks from byte prefixes
Whim\Regexbyte regular expressions
Whim\UUIDUUID parsing plus versions 4 and 7

See Encoding and Data Formats.

Time, environment, and the operating system

NamespacePurpose
Whim\Commandchild-process setup and control
Whim\DateTimedates, civil times, zones, and formatting
Whim\Envarguments, paths, and environment variables
Whim\OSowned file descriptors, accounts, and host metrics
Whim\PathPOSIX path constants
Whim\Processprocess identity, CPU time, signals, and executable lookup
Whim\ShellPOSIX shell quoting
Whim\Terminalterminal checks, paths, and size
Whim\Timedurations, monotonic instants, and wall time

See Time and Calendars and Environment, Processes, and Terminals.

Files and I/O

NamespacePurpose
Whim\Filetyped file handles and one-shot file work
Whim\Filesystempaths, directories, links, metadata, and disk space
Whim\IOhandle contracts, buffering, adapters, and copying

See Files and I/O.

Async work

NamespacePurpose
Whim\Asynctasks, futures, cancellation, groups, and limits
Whim\Channelbounded and unbounded task channels

See Tasks and Futures and Channels and Cancellation.

Network and addresses

NamespacePurpose
Whim\CIDRIP network blocks
Whim\IDNAinternational domain-name conversion
Whim\IPIPv4 and IPv6 values
Whim\IRIinternational resource identifiers
Whim\Networkendpoint, stream, listener, and connector contracts
Whim\SOCKSSOCKS5 proxy connections
Whim\TCPTCP streams, listeners, and connectors
Whim\TLSTLS settings, identities, streams, and listeners
Whim\UDPdatagram sockets
Whim\URIgeneric URI values and reference resolution
Whim\URLabsolute network URLs and origins
Whim\UnixUnix-domain streams, listeners, and pairs

See Network, TLS, and Proxies.

HTTP

NamespacePurpose
Whim\HTTP\ClientHTTP/1.1 and HTTP/2 clients and connectors
Whim\HTTP\Client\Middlewareclient cookie state and request policy
Whim\HTTP\Cookierequest cookies and Set-Cookie values
Whim\HTTP\Messagemethods, fields, requests, responses, and exchanges
Whim\HTTP\Message\Responsecommon response factories
Whim\HTTP\Routerroute registration and dispatch
Whim\HTTP\Serverserver setup, settings, and errors
Whim\HTTP\Server\Bindingstream-listener server bindings
Whim\HTTP\Server\Contextrequest context and optional capabilities
Whim\HTTP\Server\Handlerfunction, static-file, and handler contracts
Whim\HTTP\Server\MiddlewareCORS, sessions, compression, and timeouts
Whim\HTTP\Server\Responderbare and debug error responses
Whim\HTTP\Sessionsession values and settings
Whim\HTTP\Session\Storagememory and database session stores
Whim\HTTP\WebSocketWebSocket messages and connections
Whim\HTTP\WebSocket\Clientclient handshakes and connections
Whim\HTTP\WebSocket\Serverserver upgrades and connection handlers

See HTTP Messages and Cookies, HTTP Client, and HTTP Server.

Databases

NamespacePurpose
Whim\Databaseshared database, result, transaction, and pool contracts
Whim\Database\PostgreSQLevent-loop PostgreSQL driver
Whim\Database\SQLiteblocking-pool SQLite driver

See Databases.

Mail

NamespacePurpose
Whim\MessageInternet mail messages and IDs
Whim\Message\Addressmailboxes, groups, and address lists
Whim\SMTPSMTP values, envelopes, replies, and settings
Whim\SMTP\ClientSMTP transport and delivery reports
Whim\SMTP\Client\AuthenticationPLAIN, LOGIN, CRAM-MD5, SCRAM, and XOAUTH2

See Mail and MIME Messages.

Security and random data

NamespacePurpose
Whim\Hashdigests, HMAC, checksums, and streaming hashers
Whim\Passwordbcrypt and Argon2 password hashes
Whim\PseudoRandomthe process pseudo-random sequence
Whim\RandomSequencerepeatable and secure random sequence objects
Whim\SecureRandomoperating-system random bytes, strings, ints, and floats

See Hashes, Passwords, and Random Data.

Appendix A: Keywords

Whim gives keywords three reservation levels. The level controls where the word may still act as a name.

LevelFunction nameConstant nameMember name
fullnonoyes
softyesnoyes
contextualyesyesyes

A member name follows ->, ?->, or ::, or appears in a class-like member declaration. Every keyword may appear there.

Full keywords

These words may appear only as keywords or member names:

break       catch       continue    do          else
false       finally     fn          for         foreach
function    if          match       new         null
parent      return      self        static      throw
true        try         using       while

Soft keywords

These words may also name functions:

as          is

They cannot name constants because a bare use could conflict with the operator.

Contextual keywords

These words may name functions, constants, and members when their position makes the meaning clear:

abstract    array       bool        case        class
classname   const       default     dict        enum
extends     final       float       implements  in
int         interface   mixed       namespace   never
newtype     object      out         private     protected
public      readonly    string      type        use
vec         void

The _ identifier

Whim reserves _ even though it is not a keyword token. It cannot name a namespace symbol, class-like member, parameter, type parameter, or import.

$_ is valid because variable names include the $ prefix. Whim treats it as an ordinary variable.

Literal words

true, false, and null are full keywords and literal values. They may also appear in type positions as literal types.

Case

Keywords use the lowercase spellings above. Names are case-sensitive, so a different case is a different identifier:

function Match(): int {
  return 42;
}

assert!(Match() == 42);

Use such names with care. The formatter does not change identifier case.

Appendix B: Operators

The table runs from loose binding to tight binding. Parentheses override this order.

LevelOperatorsAssociation
Assignment=, +=, -=, *=, /=, %=, **=, .= and compound bit, shift, coalesce, and Boolean assignmentsright
Coalesce??right
Boolean or`
Boolean and&&left
Comparison==, !=, <, <=, >, >=, <=>none
Typeis, as, ?asnone
Pipeline`>`
Concatenation.left
Bitwise or``
Bitwise xor^left
Bitwise and&left
Shift<<, >>left
Addition+, -left
Multiplication*, /, %left
Prefix!, ~, unary + and -, prefix ++ and --right
Exponentiation**right
Postfixcalls, indexing, ->, ?->, ::, postfix ++ and --left

Comparisons and type operators do not chain. Write the intended grouping or combine complete comparisons with &&.

Collection and language operations

Several operations use construct syntax and do not belong in the precedence table:

  • length!($value) returns the length of a string or array.
  • contains!($array, $value) and contains_key!($array, $key) search arrays.
  • remove!($array, $key) removes and returns an entry.
  • swap_remove!($vec, $index) removes and returns an item without preserving vec order.
  • clone!($object, property: $value) clones an object with changed properties.
  • drop!($resource) ends ownership immediately and reports a leak.
  • debug!($value) prints a bounded structural view with its source location.
  • require! and require_once! load source files.

The compiler checks these forms by their own rules.

Appendix C: Language Constructs

A construct uses function-like syntax, but the compiler applies its own rules. This table lists every construct.

ConstructResultMain rule
assert!($test)voidthrows when a bool test is false
assert!($test, $message)voidadds a string message
clone!($object, ...)objectclones an object and may replace fields
contains!($array, $value)boolchecks array values with strict equality
contains_key!($array, $key)boolchecks an array key or index
debug!(...)nullprints source sites and bounded value details
directory!()stringreturns the current source directory
discard!($value)voidmarks a discarded result as deliberate
drop!($local, ...)voidreleases locals that hold the last strong references
embed!('./file')stringembeds a file’s exact bytes while compiling
exit!()neverexits with status zero
exit!($status)neverexits with the low eight bits of an int
file!()stringreturns the current source file
length!($value)intcounts string bytes or array items
panic!('message')neverprints a trace and exits with status 255
remove!($array, $key)valueremoves and returns one entry
swap_remove!($vec, $index)valueremoves a vec item without keeping order
remove_first!($vec)valueremoves and returns the first item
remove_last!($vec)valueremoves and returns the last item
require!($path)nullloads and runs a source file
require_once!($path)nullloads a resolved path at most once
write!(...)voidwrites to standard output
write_line!(...)voidwrites to standard output, then ends the line
write_error!(...)voidwrites to standard error
write_error_line!(...)voidwrites to standard error, then ends the line

Mutable targets

The remove constructs require a writable array place, not an arbitrary expression. drop! accepts locals and makes them undefined.

$values = vec[10, 20, 30];
$removed = remove!($values, 1);

assert!($removed == 20);
assert!($values == vec[10, 30]);

remove! preserves order when its target is a vec, so removing a non-final item shifts each later item left. remove_first! does the same at index zero. Use either form when later indexes or iteration order must stay predictable. Removing near the front of a vec takes time in proportion to the items that follow it, so repeatedly draining a vec with remove_first! takes quadratic time. Use Whim\DataStructure\Deque for a FIFO queue.

swap_remove! does not shift. It moves the last item into the removed item’s index:

$values = vec[10, 20, 30, 40];
$removed = swap_remove!($values, 1);

assert!($removed == 20);
assert!($values == vec[10, 40, 30]);

Use it for an unordered vec when removal speed matters. Its removal step takes constant time. As with any vec mutation, changing a shared vec may first copy its storage. remove_last! also takes constant time and keeps the order of all remaining items.

remove! also accepts a dict. It removes the requested key without changing the other entries. swap_remove!, remove_first!, and remove_last! accept only vecs. A missing key, invalid index, or empty vec throws OutOfBoundsError.

Output arguments

Write constructs accept any number of string, int, or float expressions. They evaluate arguments from left to right. The line forms add the host line ending after the last argument.

write_line!('count: ', 3);

They reject bool, null, arrays, objects, and callables. Convert such values first.

Debug arguments

debug! accepts any number of values. It writes one source location and value view for each argument to standard error, then returns null.

$value = debug!(42);
assert!($value == null);

The view stops after 64 collection items and 32 nested levels.

Process exit

exit! stops the process. It does not throw. A catch cannot intercept it, and pending finally blocks do not run.

panic! has the same control flow. It takes one literal string, prints the message and current stack trace to standard error, and uses status 255. Trace boundaries and sensitive parameter markers apply. Both forms run shutdown destructors.

Source paths

file!() and directory!() take no arguments. Their values belong to the source file that contains the construct, not the process entry file.

embed! takes one literal relative path. It resolves from that same source directory and reads the file while compiling. It accepts any bytes and performs no file access when the program runs. It rejects absolute paths and source read from standard input.

See Language Constructs for examples and Resources and Cleanup for drop!.

Appendix D: Grammar Guide

This guide shows the main source forms. It does not replace the parser. The chapters linked from each section define the checks and runtime rules.

The notation uses:

  • | for a choice;
  • ? for an optional part;
  • * for zero or more parts;
  • + for one or more parts;
  • quoted text for source tokens.
attributes      := attribute-list+
attribute-list  := "#[" attribute ("," attribute)* ","? "]"
attribute       := qualified-name call-arguments?

qualified-name  := "\\"? identifier ("\\" identifier)*
variable        := "$" identifier
literal         := "null" | "true" | "false"
                 | integer-literal | float-literal | string-literal
signed-integer-literal
                := "-"? integer-literal

The lexical chapter defines identifier and literal bytes. The bare identifier The grammar reserves _ even where this guide says identifier.

Source file

source-file     := shebang? source-item*

source-item     := namespace-declaration
                 | use-declaration
                 | function-declaration
                 | class-declaration
                 | interface-declaration
                 | enum-declaration
                 | type-alias
                 | newtype-declaration
                 | constant-declaration
                 | statement

A file may mix declarations and statements. A namespace declaration can apply to the rest of the file or hold a braced source body.

namespace-declaration
                := "namespace" qualified-name ";"
                 | "namespace" qualified-name "{" source-item* "}"

use-declaration := "use" use-items ";"
use-items       := use-item ("," use-item)*
                 | qualified-name "\\" "{" use-item
                   ("," use-item)* ","? "}"
use-item        := qualified-name ("as" identifier)?

See Namespaces and Imports and Loading Files.

Functions and parameters

function-declaration
                := attributes? "function" function-name type-parameters?
                   parameter-list return-type? block

parameter-list  := "(" (parameter ("," parameter)* ","?)? ")"

parameter       := attributes? parameter-modifier* type? variable
                   ("=" expression)?
parameter-modifier
                := visibility | "readonly"

return-type     := ":" type

type-parameters := "<" type-parameter ("," type-parameter)* ","? ">"

type-parameter  := variance? identifier bounds? default-type?
variance        := "in" | "out"
bounds          := ":" type ("+" type)*
default-type    := "=" type

A closure replaces the function name with a parameter list and may add a use capture list. A short closure uses fn and captures outer variables without a capture list. Its body is one expression or a block.

closure         := attributes? "function" type-parameters? parameter-list
                   capture-list? return-type? block

capture-list    := "use" "(" (variable ("," variable)* ","?)? ")"

short-closure   := attributes? "fn" type-parameters? parameter-list
                   return-type? short-closure-body

short-closure-body
                := "=>" expression | block

Visibility and readonly on a parameter promote it to a property and are valid only in a class constructor.

See Functions and Closures and Short Closures.

Classes

class-declaration
                := attributes? class-modifier* "class" identifier
                   type-parameters? class-parent? class-interfaces?
                   sealed-family? class-body

class-modifier  := "abstract" | "final" | "readonly"
class-parent    := "extends" named-type
class-interfaces
                := "implements" named-type ("," named-type)*
sealed-family   := "for" named-type ("," named-type)*

class-body      := "{" class-member* "}"
class-member    := property | class-constant | method

Properties include $ in their names. Methods and class constants do not.

property        := attributes? property-modifier+ type?
                   variable ("=" expression)? ";"

property-modifier
                := visibility | "static" | "readonly"

class-constant  := attributes? constant-modifier+ "const" type? identifier
                   "=" expression ";"
constant-modifier
                := visibility | "final"

method          := attributes? method-modifier+ "function"
                   member-name type-parameters? parameter-list
                   return-type? (block | ";")

method-modifier := visibility | "abstract" | "final" | "static"
visibility      := "public" | "protected" | "private"

A constructor parameter with a visibility word declares a promoted property. readonly may appear before or after the visibility word. A property, constant, or method has exactly one visibility word. The other modifiers may appear in any order, but each may appear only once.

See Classes and Properties and Inheritance and Visibility.

Interfaces and enums

interface-declaration
                := attributes? "interface" identifier type-parameters?
                   interface-parents? sealed-family? interface-body

interface-parents
                := "extends" named-type ("," named-type)*

interface-body  := "{" interface-member* "}"
interface-member
                := property | class-constant | method

enum-declaration
                := attributes? "enum" identifier enum-backing?
                   enum-interfaces? "{" enum-member* "}"

enum-backing    := ":" type
enum-interfaces := "implements" named-type ("," named-type)*
enum-member     := enum-case | class-constant | method
enum-case       := attributes? "case" member-name
                   ("=" expression)? ";"

See Interfaces and Sealed Families and Enums.

Aliases, newtypes, and constants

type-alias      := attributes? "type" identifier type-parameters?
                   "=" type ";"

newtype-declaration
                := attributes? "newtype" identifier type-parameters?
                   "=" type ";"

constant-declaration
                := attributes? "const" identifier "=" expression ";"

See Aliases and Newtypes.

Statements

statement       := block
                 | ";"
                 | expression ";"
                 | if-statement
                 | while-statement
                 | do-while-statement
                 | for-statement
                 | foreach-statement
                 | try-statement
                 | using-statement
                 | final-local-statement

block           := "{" statement* "}"

if-statement    := "if" "(" expression ")" block
                   ("else" (if-statement | block))?

while-statement := "while" "(" expression ")" block

do-while-statement
                := "do" block "while" "(" expression ")" ";"

for-statement   := "for" "(" expression-list? ";"
                   expression-list? ";" expression-list? ")" block

foreach-statement
                := "foreach" "(" expression "as" foreach-target
                   ("=>" foreach-target)? ")" block

foreach-target  := assignment-target
assignment-target
                := variable | property-access | static-property-access
                 | array-index | array-append
                 | tuple-destructure | dict-destructure

tuple-destructure
                := "(" destructure-item ","
                   (destructure-item ("," destructure-item)*)? ","? ")"
destructure-item
                := assignment-target
                 | assignment-target "=" expression
                 | "..." assignment-target?
dict-destructure
                := "dict" "[" (expression "=>" assignment-target
                   ("," expression "=>" assignment-target)* ","?)? "]"

expression-list := expression ("," expression)*

final-local-statement
                := "final" variable "=" expression ";"

See Statements and Loops.

Error handling and cleanup

try-statement   := "try" block catch-clause* else-clause? finally-clause?

catch-clause    := "catch" "(" type variable? ")" guard? block
guard           := "if" "(" expression ")"
else-clause     := "else" block
finally-clause  := "finally" block

using-statement := "using" "(" using-binding
                   ("," using-binding)* ","? ")" block
using-binding   := bind-target "=" expression

A try statement needs at least one catch, else, or finally clause.

See Throwing and Catching and Resources and Cleanup.

Expressions

expression      := literal
                 | interpolated-string
                 | variable
                 | constant-name
                 | "(" expression ")"
                 | tuple-literal
                 | vec-literal
                 | vec-fill
                 | dict-literal
                 | closure
                 | short-closure
                 | match-expression
                 | "new" class-expression call-arguments?
                 | "break" integer-literal?
                 | "continue" integer-literal?
                 | "return" expression?
                 | "throw" expression
                 | unary-expression
                 | binary-expression
                 | assignment-expression
                 | call-expression
                 | partial-call
                 | member-expression
                 | index-expression
                 | construct-expression

class-expression is a named class, self, parent, static, a type parameter, or an expression that yields a class-name string. The operator appendix gives the precedence that turns the broad forms above into one tree.

Calls may use positional or named arguments. ? in an argument place creates a partial call. ... in a call place creates a first-class callable or leaves later partial-call parameters open.

call-arguments  := "(" (call-argument ("," call-argument)* ","?)? ")"
call-argument   := (parameter-name ":")? (expression | "?" | "...")

... must be the sole or final placeholder. Ordinary calls do not accept placeholders.

Collection literals use these forms:

tuple-literal   := "(" expression "," ")"
                 | "(" expression "," expression
                   ("," expression)* ","? ")"

vec-literal     := "vec" "[" (vec-item ("," vec-item)* ","?)? "]"
vec-item        := expression | "..." expression
vec-fill        := "vec" "[" expression ";" expression "]"

dict-literal    := "dict" "[" (dict-item ("," dict-item)* ","?)? "]"
dict-item       := expression "=>" expression | "..." expression

See Expressions, Operators and Arithmetic, and First-Class and Partial Calls.

Match patterns

match-expression
                := "match" "(" expression ")" "{"
                   match-arm ("," match-arm)* ","? "}"

match-arm       := pattern "=>" expression

pattern         := union-pattern
union-pattern   := as-pattern ("|" as-pattern)*
as-pattern      := primary-pattern ("@" union-pattern)?
primary-pattern := variable
                 | type
                 | "(" pattern ")"
                 | tuple-pattern
                 | vec-pattern
                 | dict-pattern

tuple-pattern   := "(" pattern ("," pattern)*
                   ("," trailing-pattern)? ","? ")"
vec-pattern     := "vec" "[" (pattern ("," pattern)*)?
                   ("," trailing-pattern)? ","? "]"
dict-pattern    := "dict" "[" (dict-pattern-entry
                   ("," dict-pattern-entry)*)?
                   ("," trailing-pattern)? ","? "]"
dict-pattern-entry
                := (string-literal | signed-integer-literal) "=>" pattern
trailing-pattern
                := "..." pattern?

Variables bind. Types and literals check. @ requires both patterns to match the same value. Tuple, vec, and dict patterns may nest. Their final item may use ... to accept, check, or bind the rest.

See Match and Destructuring.

Types

type            := union-type
union-type      := intersection-type ("|" intersection-type)*
intersection-type
                := prefix-type ("&" prefix-type)*
prefix-type     := "!" prefix-type
                 | "=" prefix-type
                 | primary-type

primary-type    := built-in-type
                 | named-type
                 | literal-type
                 | range-type
                 | tuple-type
                 | vec-type
                 | dict-type
                 | array-type
                 | callable-type
                 | classname-type
                 | "(" type ")"

named-type      := qualified-name ("<" type-list ">")?
                   ("::" identifier ("<" type-list ">")?)?
                 | "self" ("::" identifier ("<" type-list ">")?)?
                 | "parent"
                 | "static"
vec-type        := "vec" ("<" type ">")? | "vec" "[" shape-items? "]"
dict-type       := "dict" ("<" type "," type ">")?
                 | "dict" "[" dict-shape-items? "]"
array-type      := "array" ("<" type "," type ">")?
callable-type   := "fn" | "fn" "(" callable-parameters? ")" ":" type
classname-type  := "classname" "<" type ">"

type-list       := type ("," type)* ","?
built-in-type   := "null" | "bool" | "int" | "float" | "string"
                 | "object" | "mixed" | "never" | "void"
literal-type    := literal | "-" (integer-literal | float-literal)
range-type      := signed-integer-literal (".." | "..=")
                   signed-integer-literal?
                 | (".." | "..=") signed-integer-literal

tuple-type      := "(" type "," ")"
                 | "(" type "," tuple-type-tail ","? ")"
                 | "(" trailing-type ")"
tuple-type-tail := type ("," type)* ("," trailing-type)?
                 | trailing-type
trailing-type   := "..." type?

shape-items     := type ("," type)* ("," trailing-type)? ","?
dict-shape-items
                := dict-shape-entry ("," dict-shape-entry)*
                   ("," dict-shape-rest)? ","?
dict-shape-entry
                := (string-literal | integer-literal) "=>" type
dict-shape-rest := "..." "<" type "," type ">"

callable-parameters
                := callable-parameter ("," callable-parameter)* ","?
callable-parameter
                := "="? type

See Runtime Type Checks, Unions, Intersections, and Ranges, and Collection and Callable Types.

Appendix E: Glossary

Artifact

A compiled Whim unit stored in a .whia file.

Array

The common type of tuples, vecs, and dicts.

Attribute

A typed value attached to a declaration, member, or parameter.

Autoloader

Code that tries to define a symbol when Whim first needs it.

Backed enum

An enum whose cases each have an int or string value.

Bound

A type rule that limits a generic type argument.

Callable

A closure, short closure, first-class function, bound method, or partial call.

Cancellation token

A value that tells a waiting operation to stop waiting.

Class family

A class and its parent and child classes.

Closure

An unnamed function with a block body and an explicit capture list.

Constant expression

A source form allowed in constants and defaults. It cannot read a local variable or $this directly, but a call inside it may run code, inspect state, or throw.

Coroutine

A call stack that may pause and later continue on the event loop.

Dict

A mutable ordered array with bool, int, or string keys.

Future

A read-only view of a value or throwable that will arrive later.

Identity

The rule by which two values refer to the same object or callable.

Interface

A set of methods, properties, constants, and constructor rules that an object must meet.

Literal type

A type that contains one scalar, constant, or enum-case value.

Newtype

A runtime tag placed on a value that must fit a backing type.

Nullable type

A union of null and another type, written null|T.

Partial call

A callable made from a call expression whose ? arguments remain open.

Reified generic

A generic whose type arguments stay available while the program runs.

Resource

An object whose lifetime code checks with using or drop!.

Sealed family

A class or interface that lists the symbols allowed directly below it.

Strong reference

A reference that keeps an object alive.

Symbol

A named class, interface, enum, function, constant, alias, or newtype.

Task

A cooperatively scheduled async call.

Tuple

An immutable fixed-size array whose positions may have different types.

Type alias

A transparent name for another type.

Type identifier

An engine-local integer used to compare or index runtime types.

Unit enum

An enum whose cases have names but no backing values.

Value semantics

Assignment and argument passing give arrays independent values. The runtime may share their storage until one value changes.

Vec

A mutable dense array with integer keys from zero.

Weak reference

A reference that observes an object without keeping it alive.

Wildcard type

_ inside another type, meaning that a nested type exists but need not match one fixed type.

Appendix F: Frequently Asked Questions

Whim uses syntax from the PHP family. It is not a PHP implementation. It has its own types, arrays, generics, async work, errors, and standard library.

Can I run PHP code as Whim code?

No. Some source may look alike, but the languages have different syntax and runtime semantics.

Why does Whim check types at runtime?

Whim can load code while it runs. Runtime checks keep each declared contract in force after that load. This includes generic arguments, ranges, callable signatures, and collection members.

Why must conditions return bool?

Whim does not guess whether null, a number, a string, or an array means true. Write the test you mean.

Why do arrays use value semantics?

A local array change should not change another variable by accident. The runtime shares storage until one copy changes, so assignment need not copy all items at once.

When should I use null, Option, or Result?

Use null|T when null cannot also be a valid T. Use Option<T> when Some(null) must differ from no value. Use Result<T, E> when failure is data that the caller should inspect. Most library failures throw.

Does Whim use threads?

Normal Whim code runs on one event loop. Tasks can overlap waits, but they do not run Whim code on several CPU cores at once. Separate bounded worker pools run blocking SQLite, file, and operating-system work.

Does Whim support Windows?

No. Whim supports macOS on x86-64 and Arm64, and glibc-based Linux on x86-64, Arm64, and RISC-V 64.

Where is the package registry?

There is none. A Git repository is a package identity. SemVer Git tags are its releases. Whim installs each graph under the current project’s vendor/.

Does whim run load packages on its own?

No. Run reads settings from the manifest, but it does not inspect the lock or vendor directory. Require vendor/autoload.whim from the application.

Will Whim keep old code working?

No. Any release may add, change, or remove language rules and library APIs.

Should I use Whim in production?

No. Whim is a toy for experiments.

What would make Whim a production project?