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.0has 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:
| Option | Effect |
|---|---|
| `–colors auto | always |
--config PATH | load 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 Nor--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
uselist and no$thisuse; - 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 thanembed!;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.
Compile and link order
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:
- creates the language core;
- loads the bundled standard-library artifact;
- reads and compiles
app.whim; - links its declarations;
- runs its file-scope statements; and
- 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:
ParserErrorfor invalid source tokens or syntax;CompilerErrorfor a rule that one unit breaks;LinkerErrorfor 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.
| Type | Values |
|---|---|
null | null |
bool | true and false |
int | signed 64-bit integers |
float | double-precision floating-point numbers |
string | byte 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:
| Type | Meaning |
|---|---|
(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:
| Escape | Byte or text |
|---|---|
\\ | backslash |
\" | double quote |
\$, \{, \} | interpolation marker as text |
\n, \r, \t | line feed, carriage return, tab |
\v, \f, \e | vertical tab, form feed, escape |
\xH, \xHH | one byte from one or two hex digits |
\O, \OO, \OOO | one 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 && $rightskips$right;true || $rightskips$right;- a non-null left side of
??skips the right side; - a null receiver for
?->skips the member access and call arguments; matchevaluates 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
protectedtopublic. - 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:
selfis the declaring class with its bound type arguments;parentis its direct parent;staticis 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:
!neveraccepts every value;!mixedaccepts no value;!!Thas the same values asT.
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.
| Type | Accepted values |
|---|---|
1..10 | 1 through 9 |
1..=10 | 1 through 10 |
0.. | zero and all larger integers |
..0 | all integers below zero |
..=0 | zero 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(): stringgetCode(): intgetFile(): stringgetLine(): intgetTrace(): vec<TraceFrame>getPrevious(): null|ThrowabletoString(): 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
| Method | Some<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 None | unchanged None |
inspect($f) | calls $f, then returns itself | returns 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
| Method | Ok<T> | Err<E> |
|---|---|---|
map($f) | Ok($f($value)) | keeps the error |
mapErr($f) | keeps the value | Err($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 itself | returns itself |
inspectErr($f) | returns itself | calls $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): voidhas($key): boolget($key): Vremove($key): Vlength(): 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): Twaits 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(): staticmarks 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 throwsFullExceptionwhen full.waitUntilSendable($cancellation)waits for space without sending a value.
Sending through a closed channel throws ClosedException.
Receiving
Receiver<T> provides:
receive($cancellation): Twaits for a value.tryReceive(): Treturns at once and throwsEmptyExceptionwhen 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|NonNegativeIntreturnsnullfor an unbounded channel.count(): NonNegativeIntreturns the buffered item count.isFull()andisEmpty()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]:
| Flag | Target |
|---|---|
TARGET_CLASS | class, interface, or enum |
TARGET_FUNCTION | named function or closure |
TARGET_METHOD | method |
TARGET_PROPERTY | property |
TARGET_CLASS_CONSTANT | class-like constant or enum case |
TARGET_PARAMETER | function, method, or closure parameter |
TARGET_TYPE_ALIAS | type alias |
TARGET_NEWTYPE | newtype |
TARGET_CONSTANT | namespace constant |
TARGET_SYMBOL | any named symbol |
TARGET_ALL | every 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 typeT.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:
src/Model/User.whim- 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:
Trifle\Clockfor clocks and sleep that tests can controlTrifle\Difffor value and text diffsTrifle\SemVerfor semantic versions and version requirementsTrifle\Argsfor command-line arguments
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:
ArrayKeyisstring|int|bool.Numericisint|float.Scalarisint|float|string|bool.Nullable<T>isT|null.NonNull,NonEmptyString,NonEmptyVec<T>, andNonEmptyDict<K, V>exclude empty values.PositiveInt,NonNegativeInt,NegativeInt, andNonPositiveIntname integer ranges.Uint8,Int8,Uint16,Int16,Uint32,Int32, andUint64name fixed bounds.Percentis0..=100;Digitis0..=9.Iterable<K, V>accepts an iterator, a value that creates an iterator, or an array.AnyTupleandAnyTupleOf<T>accept tuples of any supported length.Exclude<T, N>removesNfromT;Extract<T, U>keeps their overlap.
It also names common callables:
Predicate<T>:fn(T): boolTransform<T, U>:fn(T): UReducer<T, U>:fn(U, T): UConsumer<T>:fn(T): voidSupplier<T>:fn(): TComparator<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, andreflect_enumfind class-like symbols.reflect_type_aliasandreflect_newtypefind named types.reflect_functionandreflect_constantfind functions and constants.reflect_symbolfinds any named symbol.reflect_class_likeaccepts 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 typeT.reflect_type_of($value)reflects a value’s runtime type.reflect_type_id($id)finds the type for an engine-localWhim\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:
Base32supports its standard alphabets and optional padding.Base64supports standard and URL-safe alphabets and optional padding.Hexencodes and decodes hexadecimal text.URIpercent-encodes full URI references without changing their syntax.Urlhandles component and form encoding.Punycodeconverts international labels.QuotedPrintablehandles text and binary modes plus strict or forgiving input.EncodedWordhandles encoded words used in mail headers.UTF8\lossyreplaces 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:
GzipDeflateBrotliZstandard
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
Handlemarks an I/O value.ReadHandlereads bytes and waits for readable data.WriteHandlewrites bytes and waits for writable space.BufferedReadHandleadds byte, line, and delimiter reads.BufferedWriteHandleaddsflush.SeekHandlemoves a cursor.CloseHandlereports and closes owned state.FileDescriptorHandleexposes an ownedOS\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:
BoundedReadHandlefails after a read limit.FixedLengthReadHandleexposes an exact length.TruncatedReadHandlestops after a maximum length.ConcatReadHandlereads several sources in order.JoinedReadWriteHandlejoins separate read and write sides.TeeWriteHandlecopies writes to several targets.- sink handles discard writes or expose end-of-input reads.
SpoolHandlekeeps 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()returnsnullfor a missing environment value.get_variables()returns all current environment values.set_variable()andremove_variable()update the process environment.current_directory()andset_current_directory()handle the working path.home_directory()andtemporary_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 ornull.getAll($name)returns every value in order.has($name)checks a name.withreplaces all values under a name.withAddedappends one value.withoutremoves a name.toVec,count,isEmpty, andtoIteratorinspect 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)withapplication/jsontext($value)with UTF-8 plain texthtml($value)with UTF-8 HTMLempty($status)with no bodyredirect,see_other,temporary_redirect, andpermanent_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.
Set-Cookie
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:
Informationalcan send a 1xx response.Pushcan start an HTTP/2 server push.Secureexposes TLS security state.Upgradecan 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:
CORSfor origin, method, field, credential, and cache rules.HandlerTimeoutfor an optional per-handler deadline.RequestDecompressionfor registered content codings and size bounds.ResponseCompressionforAccept-Encodingnegotiation.Sessionfor cookie-backed stored sessions.FunctionMiddlewarefor 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_onereturns one row ornulland rejects extra rows.fetch_allreturns every row.fetch_valuereturns one selected value.transactionalbegins, 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
| Namespace | Purpose |
|---|---|
Whim | the VERSION constant |
Whim\Attribute | read attributes from symbols and members |
Whim\Autoload | register and run symbol autoloaders |
Whim\Comparison | equality, order, and Ordering |
Whim\Convert | explicit value conversion contracts |
Whim\Default | the default-value contract |
Whim\Enum | interfaces implemented by all enums |
Whim\GC | explicit cycle collection |
Whim\Marker | built-in attributes and compiler markers |
Whim\Option | Some, None, and option helpers |
Whim\Promise | the read-only async result contract |
Whim\Reference | weak references and weak maps |
Whim\Refine | common aliases, ranges, and callable types |
Whim\Reflection | read-only access to loaded declarations, types, and values |
Whim\Reflection\Attribute | attribute rules and target kinds |
Whim\Reflection\Callable | functions, methods, closures, captures, and bound arguments |
Whim\Reflection\Generic | type parameters, bindings, and type environments |
Whim\Reflection\Member | methods, properties, constants, and enum cases |
Whim\Reflection\Symbol | classes, interfaces, enums, aliases, newtypes, functions, and constants |
Whim\Reflection\Type | type forms and their parts |
Whim\Result | Ok, Err, and throwable capture |
Whim\Symbol | symbol lookup and symbol kinds |
Whim\Type | engine-local type identifiers |
Whim\Unwind | errors, exceptions, throwables, and trace frames |
See Core Types and Functions, Reflection, Option and Result, and Built-in Attributes.
Strings, numbers, and collections
| Namespace | Purpose |
|---|---|
Whim\Binary | fixed-width integer and float encoding |
Whim\Dict | eager keyed collection functions |
Whim\Float | float parsing, bit forms, and checks |
Whim\Int | integer parsing |
Whim\Iterate | iterators and lazy collection functions |
Whim\Math | arithmetic, statistics, bases, and math constants |
Whim\Range | runtime integer range objects |
Whim\Str | byte-string search and changes |
Whim\Unicode | Unicode case folding and code-point checks |
Whim\Vec | eager list functions |
See Strings, Numbers, and Binary Data, Collections and Data Structures, and Iterators.
Data structures and formats
| Namespace | Purpose |
|---|---|
Whim\BSON | BSON values, encoding, decoding, readers, and writers |
Whim\CSV | streaming CSV readers and writers |
Whim\Compression | gzip, deflate, Brotli, and Zstandard streams |
Whim\DataStructure | queue, stack, deque, heap, and priority queue |
Whim\Encoding | shared encoding errors and contracts |
Whim\Encoding\Base32 | Base32 text |
Whim\Encoding\Base64 | standard and URL-safe Base64 |
Whim\Encoding\EncodedWord | mail header encoded words |
Whim\Encoding\Hex | hexadecimal text |
Whim\Encoding\Punycode | Punycode labels |
Whim\Encoding\QuotedPrintable | quoted-printable text and bytes |
Whim\Encoding\URI | whole-URI percent encoding |
Whim\Encoding\UTF8 | UTF-8 checks and lossy repair |
Whim\Encoding\Url | percent and form encoding |
Whim\HTML | WHATWG character references and escaping |
Whim\Json | JSON values, encoding, and decoding |
Whim\MIME | media types, fields, content IDs, and parts |
Whim\MIME\MultiPart | multipart writing and parsing |
Whim\MIME\Part | text, data, and raw MIME parts |
Whim\MIME\Sniff | media-type checks from byte prefixes |
Whim\Regex | byte regular expressions |
Whim\UUID | UUID parsing plus versions 4 and 7 |
See Encoding and Data Formats.
Time, environment, and the operating system
| Namespace | Purpose |
|---|---|
Whim\Command | child-process setup and control |
Whim\DateTime | dates, civil times, zones, and formatting |
Whim\Env | arguments, paths, and environment variables |
Whim\OS | owned file descriptors, accounts, and host metrics |
Whim\Path | POSIX path constants |
Whim\Process | process identity, CPU time, signals, and executable lookup |
Whim\Shell | POSIX shell quoting |
Whim\Terminal | terminal checks, paths, and size |
Whim\Time | durations, monotonic instants, and wall time |
See Time and Calendars and Environment, Processes, and Terminals.
Files and I/O
| Namespace | Purpose |
|---|---|
Whim\File | typed file handles and one-shot file work |
Whim\Filesystem | paths, directories, links, metadata, and disk space |
Whim\IO | handle contracts, buffering, adapters, and copying |
See Files and I/O.
Async work
| Namespace | Purpose |
|---|---|
Whim\Async | tasks, futures, cancellation, groups, and limits |
Whim\Channel | bounded and unbounded task channels |
See Tasks and Futures and Channels and Cancellation.
Network and addresses
| Namespace | Purpose |
|---|---|
Whim\CIDR | IP network blocks |
Whim\IDNA | international domain-name conversion |
Whim\IP | IPv4 and IPv6 values |
Whim\IRI | international resource identifiers |
Whim\Network | endpoint, stream, listener, and connector contracts |
Whim\SOCKS | SOCKS5 proxy connections |
Whim\TCP | TCP streams, listeners, and connectors |
Whim\TLS | TLS settings, identities, streams, and listeners |
Whim\UDP | datagram sockets |
Whim\URI | generic URI values and reference resolution |
Whim\URL | absolute network URLs and origins |
Whim\Unix | Unix-domain streams, listeners, and pairs |
See Network, TLS, and Proxies.
HTTP
| Namespace | Purpose |
|---|---|
Whim\HTTP\Client | HTTP/1.1 and HTTP/2 clients and connectors |
Whim\HTTP\Client\Middleware | client cookie state and request policy |
Whim\HTTP\Cookie | request cookies and Set-Cookie values |
Whim\HTTP\Message | methods, fields, requests, responses, and exchanges |
Whim\HTTP\Message\Response | common response factories |
Whim\HTTP\Router | route registration and dispatch |
Whim\HTTP\Server | server setup, settings, and errors |
Whim\HTTP\Server\Binding | stream-listener server bindings |
Whim\HTTP\Server\Context | request context and optional capabilities |
Whim\HTTP\Server\Handler | function, static-file, and handler contracts |
Whim\HTTP\Server\Middleware | CORS, sessions, compression, and timeouts |
Whim\HTTP\Server\Responder | bare and debug error responses |
Whim\HTTP\Session | session values and settings |
Whim\HTTP\Session\Storage | memory and database session stores |
Whim\HTTP\WebSocket | WebSocket messages and connections |
Whim\HTTP\WebSocket\Client | client handshakes and connections |
Whim\HTTP\WebSocket\Server | server upgrades and connection handlers |
See HTTP Messages and Cookies, HTTP Client, and HTTP Server.
Databases
| Namespace | Purpose |
|---|---|
Whim\Database | shared database, result, transaction, and pool contracts |
Whim\Database\PostgreSQL | event-loop PostgreSQL driver |
Whim\Database\SQLite | blocking-pool SQLite driver |
See Databases.
| Namespace | Purpose |
|---|---|
Whim\Message | Internet mail messages and IDs |
Whim\Message\Address | mailboxes, groups, and address lists |
Whim\SMTP | SMTP values, envelopes, replies, and settings |
Whim\SMTP\Client | SMTP transport and delivery reports |
Whim\SMTP\Client\Authentication | PLAIN, LOGIN, CRAM-MD5, SCRAM, and XOAUTH2 |
Security and random data
| Namespace | Purpose |
|---|---|
Whim\Hash | digests, HMAC, checksums, and streaming hashers |
Whim\Password | bcrypt and Argon2 password hashes |
Whim\PseudoRandom | the process pseudo-random sequence |
Whim\RandomSequence | repeatable and secure random sequence objects |
Whim\SecureRandom | operating-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.
| Level | Function name | Constant name | Member name |
|---|---|---|---|
| full | no | no | yes |
| soft | yes | no | yes |
| contextual | yes | yes | yes |
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.
| Level | Operators | Association |
|---|---|---|
| Assignment | =, +=, -=, *=, /=, %=, **=, .= and compound bit, shift, coalesce, and Boolean assignments | right |
| Coalesce | ?? | right |
| Boolean or | ` | |
| Boolean and | && | left |
| Comparison | ==, !=, <, <=, >, >=, <=> | none |
| Type | is, as, ?as | none |
| 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 |
| Postfix | calls, 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)andcontains_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!andrequire_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.
| Construct | Result | Main rule |
|---|---|---|
assert!($test) | void | throws when a bool test is false |
assert!($test, $message) | void | adds a string message |
clone!($object, ...) | object | clones an object and may replace fields |
contains!($array, $value) | bool | checks array values with strict equality |
contains_key!($array, $key) | bool | checks an array key or index |
debug!(...) | null | prints source sites and bounded value details |
directory!() | string | returns the current source directory |
discard!($value) | void | marks a discarded result as deliberate |
drop!($local, ...) | void | releases locals that hold the last strong references |
embed!('./file') | string | embeds a file’s exact bytes while compiling |
exit!() | never | exits with status zero |
exit!($status) | never | exits with the low eight bits of an int |
file!() | string | returns the current source file |
length!($value) | int | counts string bytes or array items |
panic!('message') | never | prints a trace and exits with status 255 |
remove!($array, $key) | value | removes and returns one entry |
swap_remove!($vec, $index) | value | removes a vec item without keeping order |
remove_first!($vec) | value | removes and returns the first item |
remove_last!($vec) | value | removes and returns the last item |
require!($path) | null | loads and runs a source file |
require_once!($path) | null | loads a resolved path at most once |
write!(...) | void | writes to standard output |
write_line!(...) | void | writes to standard output, then ends the line |
write_error!(...) | void | writes to standard error |
write_error_line!(...) | void | writes 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.
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
Is Whim related to PHP?
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.