The Whim Programming Language
Whim is an experimental programming language inspired by PHP and Hack. As a research project, it explores language ideas by putting them into practice, with the hope that useful ones can contribute to PHP’s future.
Do not use Whim in production.
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.
Try Whim in the Whim playground without installing it.
Start with Installation, then write Your First Program.
Installation
Run Whim through the whim command.
Platform support
Release builds (✅ available, ❌ unavailable):
| Architecture | macOS | Linux (glibc) | Linux (musl) | FreeBSD | Windows (MSVC) |
|---|---|---|---|---|---|
| x86_64 | ✅ * | ✅ | ✅ * | ✅ * | ✅ |
| x86 | ❌ | ❌ | ❌ | ❌ | ❌ |
| AArch64 | ✅ | ✅ * | ✅ * | ✅ * | ❌ |
| ARM | ❌ | ❌ | ❌ | ❌ | ❌ |
| RISC-V | ❌ | ✅ * | ❌ | ❌ | ❌ |
| LoongArch64 | ❌ | ❌ | ❌ | ❌ | ❌ |
| PowerPC64 | ❌ | ❌ | ❌ | ❌ | ❌ |
* marks builds that are not tested in CI. RISC-V builds target 64-bit systems.
CI runs the Rust and Whim test suites on pushes to main and pull requests.
FreeBSD and Linux musl builds run only in the release workflow, without running
tests.
Shell installer
Install the latest release on macOS, Linux, or FreeBSD:
curl --proto '=https' --tlsv1.2 -sSf https://whim.sh/install.sh | bash
On Alpine Linux, install the tools needed by the installer first:
apk add bash curl ca-certificates
On FreeBSD 14.4 or later, install the tools and PostgreSQL client library first:
pkg install bash curl postgresql18-client
FreeBSD builds use the system PostgreSQL client library, including when the script does not use PostgreSQL.
Pass a version to install a specific release:
curl --proto '=https' --tlsv1.2 -sSf https://whim.sh/install.sh | bash -s -- --version=0.2.0
The installer verifies build attestations when a compatible GitHub CLI is available.
On Linux, it detects glibc or musl and downloads the matching build.
Note: Release
0.1.0has no attestation.
Windows
On 64-bit Windows 10 version 1803 or later, download
whim-<version>-x86_64-pc-windows-msvc.zip from
GitHub Releases.
Extract whim.exe and add its directory to your user PATH.
Open a new terminal and run whim --version.
Windows uses the same standard-library API. POSIX-only operations throw
Whim\Unwind\UnsupportedPlatformException; see
Windows platform limits.
Manual installation
Download the archive for your system from GitHub Releases. Put the
whim file (whim.exe on Windows) 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.99 or later. From the repository root, run:
cargo build --locked --release
The build produces the whim executable at target/release/whim.
On Windows, use the x86_64-pc-windows-msvc Rust toolchain. Install Visual Studio
Build Tools with the C++ tools and Windows SDK, plus CMake, Perl, and NASM.
The build produces target\release\whim.exe. Release builds set
RUSTFLAGS=-C target-feature=+crt-static to include the C runtime.
The package manager needs Git for Windows on PATH.
On FreeBSD, use the latest package repository for Rust 1.99 or later, then
install the build tools and PostgreSQL client library:
pkg install rust cmake git pkgconf postgresql18-client
Source files
Whim source files use .whim. Compiled artifacts use .whia.
Continue with Your First Program.
Editor Setup
Whim includes a language server in the whim command. Keep whim on the
editor’s PATH so the editor can run:
whim language-server
The language server provides basic completion, snippets, formatting, folding,
selection ranges, and lint and syntax diagnostics.
It loads whim.toml from each workspace folder or its nearest parent, using the
same format and lint settings as the CLI. Format and lint filters
also apply to files you open in the editor.
If Whim cannot find a manifest, it shows an error and checks only open files.
Formatting and diagnostics still work for those files. Run
whim init to create a manifest, then restart the language server to enable
workspace checks. Restart it after changing the config too.
Zed
Install Whim Language Support (whim-lang) from Zed’s Extensions page:
- Open Zed’s Extensions page.
- Search for
whim-langor Whim Language Support. - Select Install.
The extension’s source is available in the Whim Zed repository.
Helix
Add this to ~/.config/helix/languages.toml:
use-grammars = { only = ["whim"] }
[[language]]
name = "whim"
language-id = "whim"
scope = "source.whim"
file-types = ["whim"]
shebangs = ["whim"]
roots = ["whim.toml"]
comment-tokens = ["//"]
block-comment-tokens = { start = "/*", end = "*/" }
indent = { tab-width = 2, unit = " " }
language-servers = ["whim"]
auto-format = true
grammar = "whim"
[language-server.whim]
command = "whim"
args = ["language-server"]
[[grammar]]
name = "whim"
source = { git = "https://github.com/carthage-software/tree-sitter-whim", rev = "80d6e34e1f9b0c8cb1c23377cc276cf82e62f3d3" }
use-grammars keeps the grammar commands limited to Whim. If the file already sets it, add "whim" to its existing list instead.
Install the full highlighting query from Tree-sitter Whim:
mkdir -p ~/.config/helix/runtime/queries/whim
curl --fail --location \
https://raw.githubusercontent.com/carthage-software/tree-sitter-whim/80d6e34e1f9b0c8cb1c23377cc276cf82e62f3d3/queries/highlights.scm \
--output ~/.config/helix/runtime/queries/whim/highlights.scm
Fetch and build the grammar, then check the setup:
hx --grammar fetch
hx --grammar build
hx --health whim
Remove auto-format = true if you do not want Helix to format files when you save them.
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.
Start a project
A single file is enough for a script. For a project, create an empty directory and initialize it:
mkdir hello
cd hello
whim init
whim init creates whim.toml, src/main.whim, and tests/main.whim. It also
starts a Git repository when needed and writes the Git rules used by Whim
projects. Pass --no-git to skip Git setup.
Run the generated program:
whim src/main.whim
Use whim init when you start an application or library. The manifest lets
Whim format the whole project and manage its Git dependencies.
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) == 3u);
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 and lints 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.
Lint
whim lint
whim lint src/ tests/
whim lint --json src/ > lint.json
With no paths, whim lint finds the nearest whim.toml and checks that project.
It uses the same file discovery as fmt, with separate lint.include and
lint.exclude settings. Explicit directories ignore the inclusion list;
explicit files bypass file filters. Per-rule exclusions still apply.
All 15 rules start enabled. The command prints source diagnostics and leaves
files unchanged. Error-level findings, syntax errors, and file errors return a
nonzero status. Lower levels still print; set lint.minimum_fail_level to make
them fail the command too. --json writes diagnostics as a JSON array to standard
output; logs stay on standard error. See Linting for the JSON fields,
rules, and settings.
Language server
whim language-server
The language server speaks LSP over standard input and output. It provides
keyword completion, snippets, formatting, folding, selection ranges, and lint
and syntax diagnostics. It publishes diagnostics as open documents change and
supports document and workspace diagnostic requests. Workspace checks use open
buffers for unsaved files and read other files from disk. The server reads lint
settings at startup; restart it after changing whim.toml. It does not 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.
lint and fmt log the selected file count and a final summary at info level.
The summary includes clean files, changed files, findings by level, file errors,
elapsed time, and whether the command failed. For fmt --check, changed files
are files that need formatting. A file error does not stop checks on other files.
Use debug to inspect the loaded configuration, file filters, discovery totals,
worker count, and pipeline duration. Use trace to see why paths were skipped,
which lint rules are enabled, and how long each file spends in reading, parsing,
linting or formatting, and reporting:
WHIM_LOG=whim=debug whim lint src/
WHIM_LOG=whim=trace whim fmt --check src/
WHIM_LOG=error whim lint src/
The whim= filter limits these logs to the CLI. Logs go to standard error;
lint diagnostics and format diffs go to standard output in file discovery order.
Directory entries are sorted by name. Trace records carry the command, batch,
and file context across workers. Detailed phase timers run only at trace level.
The commands check output writes and the final buffer flush. If the receiving
process closes a pipe, they stop reporting and exit successfully, as with
whim lint src/ | head. Other output failures return a nonzero status.
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.
CLI error text goes to standard error. Lint findings go to standard output.
Program output goes to the handle used by its write calls.
Linting
whim lint checks source for common mistakes, unclear code, and security risks.
It does not run the code. All rules start enabled, and linting never changes a
file.
whim lint
whim lint src/ tests/
The first form requires a whim.toml and checks its project directory. Both
forms skip vendor/ and .git/ during directory walks.
The command logs a run summary to standard error. WHIM_LOG=whim=debug shows
configuration, discovery, and full counts; WHIM_LOG=whim=trace adds file and phase
timings. WHIM_LOG=error hides progress logs while keeping lint diagnostics.
See Log output for details.
JSON output
Use --json to write one JSON array to standard output:
whim lint --json
whim lint --json src/ > lint.json
Each entry contains:
| Field | Value |
|---|---|
path | Source path, using the same spelling as text output |
code | Rule code, lint-attribute for an invalid lint attribute, or syntax or read for a file error |
level | error, warning, info, note, or help |
message | Diagnostic message |
span | start and end objects, each with a byte offset; null for read errors |
rendered | Full plain-text diagnostic, including annotations and help; null for read errors |
Offsets start at zero, and the end offset is exclusive. They count UTF-8 bytes,
not characters. The array follows file discovery order and is empty ([]) when
there are no findings. Whim writes it in batches as files finish, so it does not
hold the whole report in memory.
JSON output has no ANSI color codes, even with --colors always. Logs and setup
errors, such as an invalid config or a missing target path, stay on standard
error. --json does not change file selection, rule settings, or exit status.
Rules
| Rule | Default level | Checks |
|---|---|---|
tagged-todo | warning | TODO comments without an owner or issue tag |
tagged-fixme | warning | FIXME comments without an owner or issue tag |
loop-does-not-iterate | warning | Loops with an unconditional break or return |
yoda-conditions | help | Comparisons with a variable before a literal or constant |
use-compound-assignment | help | Assignments such as $x = $x + 1 |
no-parameter-shadowing | warning | Catch and foreach targets that reuse a parameter name |
prefer-while-loop | note | For loops with no initializer or increment |
prefer-early-return | help | One if statement wrapping a function body |
prefer-early-continue | help | One if statement wrapping a loop body |
no-multi-assignments | warning | Chained assignments in one expression |
readable-literal | warning | Long numeric literals without underscore separators |
no-assign-in-argument | warning | Assignments used directly as call arguments |
no-assign-in-condition | warning | Assignments used directly as if or while conditions |
no-dead-store | warning | Local assignments overwritten before a read |
no-redundant-variable | warning | Final local values that are never read |
no-empty-catch-clause | warning | Catch clauses that do not handle their error |
excessive-nesting | warning | Block depth above the set limit |
cyclomatic-complexity | error | Callable or class-like branch counts above the set limit |
no-redundant-static | help | static references where a final class makes self sufficient |
no-redundant-final | help | Final methods in final classes or enums |
no-redundant-else | help | Else branches after a branch that always exits |
no-redundant-continue | help | A continue at the end of a loop body |
no-self-assignment | warning | Variables or properties assigned to themselves |
no-redundant-use | warning | Unused or same-namespace imports |
no-redundant-readonly | help | Readonly properties repeated in a readonly class |
no-redundant-nullsafe | help | Nullsafe property links made redundant by ?? |
constant-condition | help | If conditions with a result fixed by the source |
inline-variable-return | help | A local assigned only for the next return |
no-redundant-string-concat | help | Adjacent string literals joined with . |
no-empty-comment | note | Comments that contain no text |
no-literal-password | error | Literal passwords, tokens, secrets, and API keys |
no-insecure-comparison | error | Direct comparisons of password or token values |
sensitive-parameter | error | Sensitive parameters missing the marker attribute |
no-debug-symbols | warning | debug! calls in application code |
disallowed-symbols | warning | Exact symbols named in the rule settings |
Use a tag such as TODO(#123), TODO(@owner), or FIXME(owner).
no-dead-store checks each function, method, or closure on its own. It tracks
branches separately and counts closure captures as reads. It skips parameters,
scoped bindings, variables used in catch or finally blocks, and names that start
with an underscore. It reports an overwritten assignment, not every unused
variable.
no-redundant-variable reports the last value written to a local when no later
code reads it. It keeps loop-carried reads, branch joins, closure captures, and
finally blocks in scope. Its help keeps expressions that have side effects.
The security rules infer sensitive values from names ending in password,
token, secret, apikey, or api_key, without regard to case. They provide
local checks, not a proof that a program is secure. Use Whim\Hash\equals() for
secret string comparisons and Whim\Password\verify() for password hashes.
sensitive-parameter accepts the exact Whim\Marker\SensitiveParameter
attribute, including an imported alias, and skips Boolean-only parameters.
Scoped lint attributes
Use Whim\Lint\Allow, Warn, Deny, and Forbid to set a rule’s level
on a declaration:
use Whim\Lint\{Allow, Deny};
#[Deny('no-debug-symbols')]
class HeaderParser {
#[Allow(
rule: 'no-insecure-comparison',
reason: 'The token is not security-sensitive',
)]
public function hasToken(
string $value,
#[Allow(
rule: 'sensitive-parameter',
reason: 'This is an HTTP grammar token',
)]
string $token,
): bool {
return $value == $token;
}
}
Allow suppresses the rule. Warn reports a warning. Deny reports an error.
Forbid reports an error and prevents any nested scope from lowering its level.
Trying to lower a forbidden rule produces an error on the conflicting attribute,
even when the code has no finding for that rule. A nested Deny keeps an
enclosing Forbid in force.
The setting covers the declaration, its signature, and its contents. It ends
at the declaration’s boundary. Nested declarations can override an outer
Allow, Warn, or Deny. Attributes follow source order; the last setting for
a rule wins unless an earlier Forbid prevents it. The setting follows source
scope, not calls or inheritance.
All four attributes are repeatable. They support classes, interfaces, enums,
functions, methods, closures, parameters, properties, constants, enum cases,
type aliases, newtypes, and files. Each takes a required string $rule and an
optional null|string $reason = null, both exposed as public properties.
Use one attribute per rule.
Use #![...] to set a rule’s level for the whole file:
namespace App;
use Whim\Lint\Allow;
#![Allow(
rule: 'no-debug-symbols',
reason: 'This script inspects runtime values',
)]
debug!(vec[1, 2, 3]);
File settings cover all code and comments in the file, including code before
the attribute and code in other namespaces. File attributes follow source
order, and each name uses the namespace and imports at its location.
Declaration attributes override file Allow, Warn, or Deny settings,
even when the file attribute appears later. A file Forbid prevents any
declaration or later file attribute from lowering the level.
Arguments may use positions or names. Named arguments can appear in either
order, and a positional rule can be followed by reason: .... The reason
explains the setting; the linter only evaluates the rule argument.
Rule names must use exact lint codes. The linter reads string literals, parentheses, and string concatenation, so these are equivalent:
use Whim\Lint\Allow;
#[Allow('sensitive-parameter')]
#[Allow(rule: 'sensitive' . '-' . 'parameter')]
function token_value(string $token): string {
return $token;
}
The linter does not resolve constants or run code to read a rule name. An
unknown rule, missing rule argument, duplicate or extra argument, non-string
rule name, or rule expression that it cannot read produces a lint-attribute
error. These errors cannot be suppressed or lowered by another attribute or
by rule settings.
Project settings supply the starting policy. Warn, Deny, or Forbid can
enable a disabled rule within a declaration or file. File and per-rule
exclusions still apply. The CLI and language server use the same attribute
rules, and minimum_fail_level still determines whether a warning fails the
command.
Settings
File patterns use / and are relative to the manifest directory. Exclusions win.
Explicit directories ignore include; explicit files bypass both file filters.
Per-rule exclusions apply to every file.
[lint]
include = ["**/*.whim"]
exclude = ["src/generated/**"]
minimum_fail_level = "error"
[lint.rules.tagged-todo]
level = "warning"
exclude = ["tests/**"]
[lint.rules.yoda-conditions]
mode = "deny"
[lint.rules.no-assign-in-condition]
ignore-while-statements = true
[lint.rules.excessive-nesting]
threshold = 7
function-like-threshold = 4
[lint.rules.readable-literal]
min-digits = 5
[lint.rules.prefer-early-return]
max-allowed-statements = 0
[lint.rules.no-empty-comment]
preserve-single-line-comments = true
[lint.rules.cyclomatic-complexity]
threshold = 15
method-threshold = 8
[lint.rules.disallowed-symbols]
symbols = [
'Legacy\run',
{ name = 'Legacy\Client', help = 'Use Modern\Client.', level = 'error' },
]
[lint.rules.no-literal-password]
enabled = false
Each rule accepts enabled, level, and exclude. Rule levels and
minimum_fail_level accept error, warning, info, note, or help.
minimum_fail_level controls the exit status without hiding lower-level findings.
yoda-conditions.mode defaults to require; choose deny to keep variables on
the left. no-assign-in-condition.ignore-while-statements defaults to false
and covers both while and do/while. excessive-nesting.threshold defaults
to 7; its optional function-like-threshold counts each function body from 1.
The two early-exit rules allow no wrapped statements by default.
readable-literal.min-digits defaults to 5. no-empty-comment checks line and
block comments unless preserve-single-line-comments is true.
cyclomatic-complexity.threshold defaults to 15. Its optional
method-threshold also checks each method.
disallowed-symbols uses exact, case-sensitive symbol names. It checks
declarations, imports and aliases, attributes, types, patterns, direct calls,
construction, constants, and static members. A leading \ in a setting is
optional. Each entry may set its own help text and level. An empty list reports
nothing. Dynamic callable or class values and late-bound static references do
not have one source-resolved name, so the rule skips them.
The language server uses each workspace’s whim.toml, including file and rule
exclusions. These filters also apply to open files. Editors can request a full
workspace check when a manifest exists; without one, Whim reports the missing
config and checks only open files. Open buffers take precedence over disk
contents. Restart the server after creating or changing the config.
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 tokens #[, #![, and #{ start a declaration attribute list,
a file attribute list, and an object shape, respectively. Each is a single
token: whitespace or comments cannot separate its characters.
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 #[, #![, or #{.
Number literals
Unsuffixed 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.
Append u or U for an unsigned 64-bit integer. Both spellings have the same
meaning in every base. The optional i or I suffix marks a signed integer;
it has the same meaning as no suffix.
assert!(42u == 42U);
assert!(42 == 42i);
assert!(42i == 42I);
assert!(0xffu == 255u);
assert!(0b1010U == 10u);
assert!(0o755u == 493u);
assert!(18_446_744_073_709_551_615u == Whim\Math\UINT_MAX);
A suffix follows the digits without a gap. Float literals cannot take an
integer suffix. An out-of-range literal is a compile error; an unsuffixed
literal does not become unsigned when it exceeds the signed maximum.
The compiler rejects a negated nonzero unsigned literal such as -24u.
-0u is valid and equals 0u.
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. fresh is a full keyword and a primitive type. It cannot name a
top-level declaration; $fresh remains a valid variable name.
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;
file!()anddirectory!();- the platform information constructs;
embed!with a literal relative path;- unary and binary operators;
- conditional expressions whose condition and both branches are constant expressions;
- tuple, vec, and dict literals, including vec and dict spreads;
- a
fnclosure with no captures; - 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.
fresh!() cannot appear in a constant expression. An indirect call that reaches
it during an initializer throws TypeError. Create and store a fresh value in
ordinary runtime code instead.
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);
A fn closure may use an expression or block body. Its parameters and locals
are not captures. The compiler rejects closures that need outer variables or
$this:
const DOUBLE = fn(int $value): int => $value * 2;
const MAKE_READER = fn(int $value): fn(): int {
return fn(): int => $value;
};
assert!((DOUBLE)(21) == 42);
assert!((MAKE_READER)(42)() == 42);
MAKE_READER captures nothing. When called, it creates a closure that captures
the supplied $value.
Forms that do not qualify
A constant expression cannot use:
- a variable or
$this; - assignment, indexing, or a property read;
- interpolation;
- a closure with captures;
match,throw, or a partial call;- language constructs other than those listed above;
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 seven scalar value kinds.
| Type | Values |
|---|---|
null | null |
bool | true and false |
int | signed 64-bit integers |
uint | unsigned 64-bit integers, from 0 through 18,446,744,073,709,551,615 |
float | double-precision floating-point numbers |
string | byte strings |
fresh | opaque identities created by fresh!() |
Integers and floats stay distinct:
assert!(1 is int);
assert!(1u is uint);
assert!(1.0 is float);
assert!(1u != 1);
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.
fresh!() creates an identity distinct from every identity this VM has already
created. Copying it preserves that identity:
$a = fresh!();
$b = fresh!();
$copy = $a;
assert!($a is fresh);
assert!($a != $b);
assert!($a == $copy);
$names = dict[$a => 'first', $b => 'second'];
assert!($names[$copy] == 'first');
Fresh values support == and !=, type checks, and dictionary keys. They have
no order, arithmetic, bit operations, or implicit string or bool conversion.
Even copies reject order comparisons such as $a <= $copy and $a <=> $copy.
They cannot index a vec, tuple, or string.
$a as uint gives an unsigned integer for explicit use with APIs that need
one. Equal identities give equal integers, and distinct identities give
distinct integers during that VM lifetime. The integer has no promised value
across runs and gives no ordering contract. Casting an integer back to fresh
fails, including an integer obtained from a fresh value. Casting a fresh value
to fresh preserves it.
If the VM exhausts its identity space, fresh!() throws OverflowError;
further calls also fail. Separate VMs may reuse representations. Fresh values
are not persistent IDs, distributed IDs, or secrets.
Whim\Refine\Scalar and Whim\Refine\ArrayKey include fresh.
Whim\Refine\Numeric does not. JSON and BSON reject fresh values and keys;
cast explicitly to a supported type when encoding a diagnostic representation.
Such an encoding cannot restore the identity.
Arrays
Whim has three array forms:
| Type | Meaning |
|---|---|
(A, B) | an immutable, fixed-size tuple |
vec<T> | a mutable list with uint 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.
An object shape, such as
#{ name: string, ... }, checks public instance properties and their current
values. Omit ... to require exactly the listed public properties.
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, 42u, and 'yes' each describe one value. Literal types make unions,
ranges, enum cases, and constants precise.
String length types
Put an integer or integer range after string to limit its byte length:
| Type | Accepted strings |
|---|---|
string[0] | empty strings |
string[5] | strings with five bytes |
string[1..] | strings with at least one byte |
string[1..=64] | strings with one through 64 bytes |
The range uses the same bounds as an integer range. Its lower bound is
inclusive. .. excludes the upper bound, while ..= includes it.
Whim treats string[0] as '', string[1..] as string&!'', and
string[0..] as string.
String lengths count bytes. For example, the UTF-8 form of é has two bytes:
assert!('é' is string[2]);
String length types work in aliases, parameters, properties, generic bounds, casts, and patterns.
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) == 3u);
assert!($text[1] == 'b');
A string length type limits the same byte count:
function read_name(string[1..=64] $name): string {
return $name;
}
assert!('Ada' is string[3]);
string[0] is the same type as ''. string[1..] is the same type as
string&!''.
An index accepts int or uint. A negative or out-of-range 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 uint 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, uint, string, and fresh keys. It keeps insertion order:
$scores = dict['Ada' => 10, 'Grace' => 12];
$scores['Ada'] = 11;
$scores['Linus'] = 9;
assert!($scores['Ada'] == 11);
assert!(length!($scores) == 3u);
Keys do not convert. 1, 1u, '1', and true are four different keys.
Copies of a fresh value name the same entry. Its uint cast names a separate
entry.
The unsigned maximum is a valid key. Vec, tuple, and string positions
accept either int or uint. Both 1 and 1u name position one. Negative
positions and positions outside the value’s length are out of bounds.
Collection lengths and iteration keys for vecs and tuples use uint.
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 uint
keys. A dict keeps its keys.
Reading entries
An ordinary indexed read requires an existing index. Reading a missing vec
position, tuple position, or dict key throws OutOfBoundsError. Use ?? to
provide a fallback for a missing or null entry:
$values = dict['ready' => true];
assert!(!contains_key!($values, 'missing'));
assert!(($values['missing'] ?? false) == false);
contains_key! checks existence, including entries whose value is null.
contains! checks values:
$values = vec[10, 20];
assert!(contains_key!($values, 1));
assert!(contains_key!($values, 1u));
assert!(contains!($values, 20));
length! works on every array and on strings and returns uint.
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 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!(1u != 1);
assert!(1u != 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:
- any pair of int, uint, and 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.
Numeric ordering compares mathematical values without first rounding integers to float. It can find equal magnitudes where strict equality is false:
assert!(1u <= 1);
assert!((1u <=> 1) == 0);
assert!(0u > -1);
assert!(9_007_199_254_740_993u > 9_007_199_254_740_992.0);
assert!(Whim\Math\UINT_MAX < 18_446_744_073_709_551_616.0);
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 when it is present, initialized, and
non-null. Otherwise it evaluates and returns the right side. False, zero, and
empty strings are present values:
assert!((0 ?? 10) == 0);
assert!((false ?? true) == false);
assert!((null ?? 10) == 10);
On the left of ??, property and index accesses probe the whole access path.
A missing property, uninitialized property, missing index, or null at any step
selects the fallback. This includes static properties and indexes into vecs,
dicts, tuples, and strings. Parentheses around an access preserve this behavior.
$settings = dict[];
assert!(($settings['display']['theme'] ?? 'light') == 'light');
$settings['display'] = dict['theme' => 'dark'];
assert!(($settings['display']['theme'] ?? 'light') == 'dark');
Receivers and keys are evaluated at most once, from left to right. Once a step
selects the fallback, subsequent keys are skipped. The right side is evaluated
only when needed. Invalid receiver or key types and visibility errors still
throw. An undefined local variable still throws UndefinedVariableError.
Calls and other computations execute normally, including their arguments.
For example, transform($values['key']) ?? $default performs an ordinary read
of $values['key'] before calling transform. Exceptions from a receiver,
key expression, or call propagate.
??= probes the final assignment target and writes the right side only when
that target is missing, uninitialized, or null. It returns the retained or
assigned value. A declared property can be initialized, and an existing dict
can receive a new key:
$settings = dict[];
assert!(($settings['theme'] ??= 'light') == 'light');
assert!(($settings['theme'] ??= 'dark') == 'light');
Every intermediate value must already exist, be initialized, and have the
appropriate type. ??= never creates intermediate containers. For example,
$settings['display']['theme'] ??= 'light' requires an existing, non-null
$settings['display']. A missing parent throws before the right side runs.
All normal assignment checks apply, including property types, visibility,
readonly restrictions, and collection bounds. Missing vec positions are not
created by ??=.
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);
Conditional expressions
$condition ? $then : $otherwise chooses one expression:
$ready = true;
$label = $ready ? 'ready' : 'waiting';
assert!($label == 'ready');
Whim evaluates the condition once and requires bool, just like if. A
non-boolean condition throws TypeError before either branch runs. Only the
chosen branch runs, and its value becomes the result. The branches may produce
different types; ordinary boundary checks still apply to the chosen value.
The chosen branch may instead transfer control with return, break,
continue, or throw. It then produces no value for the enclosing expression:
function first_true(vec<bool> $values): int {
foreach ($values as $value) {
return $value ? 42 : continue;
}
return 0;
}
assert!(first_true(vec[false, true]) == 42);
In return $value ? break : continue, either branch leaves the expression, so
the outer return does not run. break and continue keep their enclosing loop
targets. Existing finally and using cleanup rules apply. A condition that
transfers control also skips the branches. Both branches must still be legal
in their surrounding scope.
The operator binds more tightly than assignment and more loosely than ??,
||, and &&. It associates to the right: $a ? $b : $c ? $d : $e means
$a ? $b : ($c ? $d : $e). Both branches allow assignments. All three operands
are required; use ?? when choosing a fallback for null.
Conditional expressions are also valid in constant-expression positions when the condition and both branches meet those positions’ existing rules.
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.
Two uint operands produce a checked uint. Mixed int and uint operands
throw IncompatibleOperandsError for +, -, *, %, and **, even when
both values are small. Cast explicitly to choose the integer kind:
assert!(2u + 3u == 5u);
assert!((2 as uint) + 3u == 5u);
assert!(3u - 2u == 1u);
Unsigned subtraction below zero throws UnderflowError; addition and
multiplication above Whim\Math\UINT_MAX throw OverflowError. Negating an
unsigned variable returns 0u for zero and throws UnderflowError otherwise.
The compiler rejects a negated nonzero unsigned literal.
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
/ accepts any pair of int, uint, and float, and always returns float:
assert!(10 / 2 == 5.0);
assert!(7 / 2 == 3.5);
% accepts two ints or two uints and keeps that kind. A signed 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. Operand kinds are checked first: 1u % 0 is an incompatible
pair, while 1u % 0u divides by zero. For an exact unsigned quotient, use
Whim\UInt\div(7u, 2u), which returns 3u.
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.
uint ** uint returns a checked unsigned result. 0u ** 0u is 1u;
1u raised to any unsigned power is 1u. Any float operand selects floating
arithmetic, so 2u ** -1.0 is 0.5.
Bit operators
&, |, and ^ accept two ints or two uints. ~ accepts either integer
kind. They preserve that kind; ~0u is Whim\Math\UINT_MAX.
The left operand of << and >> may be int or uint. The count may independently
be int or uint, from 0 through 63; other counts throw ArithmeticError.
The result keeps the left operand’s kind. Left shifts discard high bits.
Signed right shifts extend the sign; unsigned right shifts insert zeros.
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, uint, and float targets. Unsigned steps preserve uint: $value++
adds an unsigned one, while $value += 1 rejects an unsigned target.
If arithmetic or a declared type check fails, the target keeps its old value.
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, uints, and floats as text. It rejects other values.
Unsigned values print all their decimal digits without a suffix. Interpolation
and output use the same rule.
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.
??= probes the final property or index and assigns only if it is missing,
uninitialized, or null. Every intermediate parent must already exist and have
the appropriate type; missing parents throw before the right side runs. No
intermediate containers are created. The write obeys the same type,
visibility, readonly, and bounds checks as ordinary assignment.
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 present, initialized, 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;condition ? then : otherwisechecks a boolean condition once and evaluates only the chosen branch;- a destructuring default runs only for a missing position.
0, false, and '' are not null, so ?? keeps them.
Property and index paths on the left of ?? stop at the first missing,
uninitialized, or null step and select the fallback. Later keys are skipped.
Receivers and evaluated keys run once. Invalid types, visibility violations,
undefined locals, and exceptions from calls still throw. See
coalescing for examples and the
boundary between probing an access path and evaluating a computation.
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.
Blocks group statements as the bodies of declarations and control-flow
constructs, such as functions, if, loops, try, and using. Standalone
{ ... } and {} blocks are syntax errors, including inside another body.
Named functions, classes, interfaces, enums, global constants, type aliases, newtypes, imports, and file attributes belong at the top level of a file or namespace. They cannot appear inside function bodies or control-flow blocks. Use a closure when you need a function value inside a body.
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.
Every expression statement needs an expression; ; alone is a syntax error.
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, and 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 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.
Wildcard patterns
_ accepts any value without binding a variable. As a fallback arm, it must
come last. Inside another pattern, it ignores that part of the value:
$outer = 'outer';
assert!(match (42) { _ => $outer } == 'outer');
$second = match ((1, 'two')) {
(_, $value) => $value,
_ => 'other',
};
assert!($second == 'two');
Variable patterns
A variable pattern accepts any value and binds that value inside the selected arm:
$description = match (42) {
$value => 'value:' . $value,
};
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');
_ also marks 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. Bindings become available to the arm expression after every check succeeds. 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) as int),
_ => 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 string, integer, or boolean literal keys. true, 1, and
'1' are distinct keys, as are false, 0, and '0'. Without ..., the
pattern 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'));
Rest patterns and captures
In tuple, vec, and dict patterns, ... permits a remainder. A pattern after
... checks every remaining value and must not introduce bindings inside its
elements, entries, properties, alternatives, or nested rests.
Two forms can bind the whole remainder: ...$rest and
...$rest @ pattern. In the second form, pattern checks each remaining value
and must contain no bindings. The capture goes on the left of @.
Parentheses do not change these rules.
$result = match (vec[1, 'two', 'three']) {
vec[$first @ int, ...$strings @ string] => ($first, $strings),
_ => null,
};
assert!($result == (1, vec['two', 'three']));
Tuple and vec rests bind a vec. Dict rests bind a dict that preserves the remaining keys and their order. An empty remainder passes its element checks and binds an empty collection. If an element fails, the entire arm fails.
...#{ value: int } and ...$rest @ #{ value: int } are valid.
...#{ $value }, ...($x, $y), ...vec[...$inner], and
...$rest @ #{ $value } are compile errors. Rest captures do not collect
individual fields into new collections.
Pattern intersections
left & right requires both patterns to match the same value. It binds more
tightly than @ and |, and can combine type checks with destructuring:
$pair = match ((3, 4)) {
($x, $y) & (int, int) => ($x, $y),
_ => null,
};
assert!($pair == (3, 4));
Object patterns
#{ name: pattern } matches public instance properties. It uses the same exact
and open property-set rules as object shape types.
A missing, inaccessible, or uninitialized required property rejects the arm.
Private, protected, and static properties are ignored for exactness.
An entry may contain any pattern, including a nested shape. #{ $value } is
shorthand for #{ value: $value }; this shorthand is available only in
patterns. Write #{ value: $other } to bind a different name. Add ... to allow
additional public properties.
Inside a collection rest, object patterns follow the
rest binding rules:
...$rest @ #{ value: int } captures the whole remainder, while
...#{ $value } is a compile error.
A named type can precede an object pattern directly. Foo #{ $value } has the
same matching and binding behavior as Foo & #{ $value }. This also works
with generic names and nested patterns:
use Whim\Result\Ok;
use Whim\Result\Err;
function describe_result(mixed $result): string {
return match ($result) {
Ok<string> #{ value: '' } => 'empty',
Ok<string> #{ $value } => $value,
Err<int> #{ error: $code @ 400..=499 } => 'client error: ' . $code,
Err<int> #{ $error } => 'error: ' . $error,
_ => 'other',
};
}
assert!(describe_result(new Ok::<string>('hello')) == 'hello');
assert!(describe_result(new Err::<int>(404)) == 'client error: 404');
Intersections check the left pattern before the right. Once an object pattern’s layout matches, its listed property values are captured in source order before its nested patterns are checked. The selected arm receives those captured values, even if resolving a nested type invokes an autoloader that changes the original object. Bindings remain local to the selected arm; unsuccessful arms and union alternatives do not expose partial bindings. Property names cannot repeat within a shape, and the usual duplicate-binding and union-layout rules apply.
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.
Fresh identities
fresh!() takes no arguments and creates a fresh scalar.
Every executed call creates a distinct identity within the VM lifetime. Copies
keep their identity, and skipped branches create none:
$a = fresh!();
$b = fresh!();
assert!($a != $b);
The construct runs at runtime. It cannot run in a constant expression, even through a function call. Compiled artifacts store the generation instruction and create identities when executed.
Array and string constructs
length!($value)returns the byte length of a string or the item count of an array, as auint.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].
Sequencing expressions
sequence!($first, ..., $last) evaluates each expression once, from left to
right, and produces the last expression’s value and type. It requires at least
one expression and allows a trailing comma:
$result = sequence!(
$value = 10,
$value *= 2,
$value + 1,
);
assert!($result == 21);
assert!($value == 20);
Each earlier expression behaves like an expression statement, including
#[MustUse] checks. Use discard! on an individual expression to deliberately
ignore a required result. If the whole sequence is an expression statement,
the last expression also follows the usual discard rules.
A sequence shares the surrounding variable scope and control flow. return
exits the enclosing callable, break and continue target the enclosing
loops, and an exception skips the remaining expressions. Normal cleanup rules
still apply. The compiler emits the expressions directly without a function
call or closure.
Use it in a match arm that needs several expressions:
$input = ' hello ';
$value = match ($input) {
'' => 'empty',
$_ => sequence!(
$trimmed = Whim\Str\trim($input),
assert!(length!($trimmed) > 0),
$trimmed,
),
};
assert!($value == 'hello');
Only the selected arm evaluates its sequence. The arguments are expressions;
statements such as if, foreach, and try still belong in statement bodies.
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 evaluates one expression,
which must produce a 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.
A non-string message throws TypeError before the panic occurs. Errors
thrown while evaluating the message also propagate normally. These errors
can be caught and run pending finally blocks.
if (!contains_key!($states, $name)) {
panic!("the state table has no entry for {$name}");
}
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.
Platform information
These constructs take no arguments and return strings describing the compilation target. This defaults to the platform running Whim. The compiler stores these values as string constants.
| Construct | Value | Examples |
|---|---|---|
cpu_architecture!() | CPU architecture | 'x86_64', 'aarch64', 'riscv64' |
operating_system!() | Operating system | 'linux', 'macos', 'freebsd', 'windows' |
operating_system_family!() | Operating system family | 'unix', 'windows' |
shared_library_prefix!() | Shared library filename prefix | 'lib', '' |
shared_library_suffix!() | Shared library filename suffix, including the dot | '.so', '.dylib', '.dll' |
shared_library_extension!() | Shared library extension without the dot | 'so', 'dylib', 'dll' |
executable_suffix!() | Executable filename suffix, including a dot if present | '', '.exe' |
executable_extension!() | Executable extension without the dot | '', 'exe' |
You can use them in constant expressions, including constant declarations, parameter defaults, property defaults, and attributes:
const PLATFORM = operating_system!() . '/' . cpu_architecture!();
write_line!(PLATFORM);
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.
file!() and directory!() also work in constant expressions:
const SOURCE_FILE = file!();
const SOURCE_DIRECTORY = directory!();
assert!(SOURCE_FILE == file!());
assert!(SOURCE_DIRECTORY == directory!());
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, an optional where clause, 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 closure is an anonymous function you can save in a variable or pass as arguments to other functions. You can create the closure in one place and then call the closure elsewhere to evaluate it in a different context. Unlike functions, closures can capture values from the scope in which they’re defined.
Bodies
A closure uses fn:
$double = fn(int $value): int {
return $value * 2;
};
assert!($double(21) == 42);
Like a named function, a closure may have type parameters, typed
parameters, defaults, a return type, a where clause,
attributes, and a block body.
A 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.
Captures
A closure captures each outer variable that its body uses. Capture is by value at creation time. Parameters and locals assigned before use are not captures.
$offset = 10;
$add = fn(int $value): 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.
$this
A closure made in an instance method may use $this:
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 Callables and Partial Function Application
Whim creates a first-class callable with foo(...). Partial function application
uses ? to leave arguments open. Both produce fn values, as closure expressions
do.
First-class callables
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 function application
? 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.
You can apply partial function application to an existing callable. 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.
The coalescing operators can read a fallback for
an uninitialized property or initialize it with ??=.
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. It may not change a static property to an instance property or an instance property to static. These rules also apply to properties declared through constructor promotion.
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, uint, 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>,
Whim\Enum\BackedEnum<uint>, 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.
Backing values retain their kind. A uint-backed enum requires unsigned case values, and a signed lookup does not find an unsigned case of the same magnitude.
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, integer ranges, string length limits, 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 first checks the whole destination type. If the value already
fits, it keeps its value and numeric kind. Otherwise it tries numeric
conversion and throws TypeError if no valid conversion fits.
$value = 42 as int;
Numeric conversions follow these rules:
| From | To | Requirement |
|---|---|---|
int | uint | zero or greater |
uint | int | at most Whim\Math\INT_MAX |
int or uint | float | rounds to the nearest binary64 value |
float | int | finite, whole, and in [-2^63, 2^63) |
float | uint | finite, whole, and in [0, 2^64) |
Casts never wrap or truncate a fractional float. They do not parse strings, convert booleans, or convert the elements of a collection.
assert!((42 as uint) == 42u);
assert!((42u as int) == 42);
assert!((42u as float) == 42.0);
assert!((42.0 as uint) == 42u);
assert!((42u as (int|uint)) is uint);
assert!((42.0 as (uint|int)) == 42u);
assert!((42.0 as (int|uint)) == 42);
assert!((1.5 ?as uint) == null);
When conversion is needed, a union tries its numeric branches in source order.
Aliases and type parameters follow the types they name. Each result must fit
its branch, including range and literal limits. Intersections retain all their
constraints; a negated type alone does not choose a numeric conversion.
For example, 42.0 as (0u..=10u|int) produces signed 42.
Float conversion can lose integer precision. Whim\Math\UINT_MAX as float
rounds to 2^64, which cannot convert back to uint.
A cast to a newtype adds that newtype’s tag after checking the backing type.
It does not convert an incompatible backing value: convert to uint before
tagging a signed value with a uint-backed newtype.
?as
$value ?as T follows the same checks and numeric conversions, returning
null when they fail.
$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 {}
Signed integer, unsigned integer, and float literal types remain distinct.
1, 1u, and 1.0 each accept only their own kind.
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.
Unsigned bounds select an unsigned range: 1u..=20u, 1u.., and ..=20u
accept only uints. Missing bounds extend to zero or the unsigned maximum.
Both written bounds must have the same kind; 1..=20u is a compile error.
0u.. is the whole unsigned domain, while ..0u is empty. Signed ranges such
as 0.. continue to reject uints.
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);
String length types
Square brackets after string limit its byte length:
| Type | Accepted lengths |
|---|---|
string[5] | exactly five bytes |
string[1..] | one byte or more |
string[..64] | fewer than 64 bytes |
string[1..=64] | one through 64 bytes |
These types use signed integer bounds. Unsigned bounds such as string[4u]
are compile errors. Empty ranges accept no value.
type Name = string[1..=64];
function greet(Name $name): string {
return 'Hello, ' . $name;
}
Whim treats string[0] as '', string[1..] as string&!'', and
string[0..] as string.
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.
Structural Types
Whim can describe the parts of arrays, objects, 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, uint, string, bool, or fresh. 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.
Keys may be string, integer, or boolean literals. As in dict values, true,
1, and '1' are distinct keys, as are false, 0, and '0'.
type UserRow = dict['id' => int, 'name' => string];
$user = dict['id' => 1, 'name' => 'Ada'];
assert!($user is UserRow);
type Partition<T> = dict[true => vec<T>, false => vec<T>];
$partition = dict[true => vec[1, 3], false => vec[2, 4]];
assert!($partition is Partition<int>);
assert!(!(dict[1 => vec[1, 3], 0 => vec[2, 4]] is Partition<int>));
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 Named = dict['name' => string, ...];
type ScoredUser = dict['id' => int, 'name' => string, ...<string, int|float>];
$user = dict['id' => 1, 'name' => 'Ada', 'score' => 9.5];
assert!($user is ScoredUser);
Object shapes
#{ name: string } accepts an object with exactly one public instance property,
name, whose current value is a string. Add a final ... to allow other public
instance properties:
type NamedObject = #{ name: string, ... };
type WithValue<T> = #{ value: T, ... };
class Person {
public string $name = 'Ada';
public int $age = 36;
}
$person = new Person();
assert!($person is NamedObject);
assert!($person is #{ age: int, name: string });
assert!(!($person is #{ name: string }));
Property names are case-sensitive identifiers, without $. Required properties
must exist, be public instance properties, be initialized, and have values that
match their corresponding types. Inherited public properties count. Private,
protected, and static properties do not participate, even when the check runs
inside their declaring class. An uninitialized required property rejects a
check; it does not throw an uninitialized-property error. An extra public
property still counts toward exactness when it is uninitialized.
#{} accepts objects with no public instance properties. #{ ... } accepts
any object, just like object. Neither form accepts an array or another
non-object value. Object shapes have a bare ... rest marker; unlike dict
shapes, they do not have a typed rest entry. Property names cannot repeat.
Trailing commas are allowed.
Shapes check current values, independently of a property’s declared type. A
mixed property holding an integer can satisfy an int entry. A successful
check does not change the property’s declared type, visibility, or readonly
rules. Mutating the object can make a later check fail, including when the
object is held inside a vec, dict, or tuple.
Place a shape after a named type to combine the nominal and structural checks.
Name #{ ... } is shorthand for Name & #{ ... }, including generic names:
use Whim\Result\Ok;
$value = new Ok::<string>('hello');
assert!($value is Ok<string> & #{ value: string & !'' });
assert!($value is Ok<string> #{ value: string & !'' });
The name and shape form one type: !Name #{ value: int } negates the entire
combination, like !(Name & #{ value: int }). Closed and open property-set
rules apply equally to both spellings.
Shapes work in aliases, generic arguments and bounds, callable signatures,
parameter and return types, property types, is, as, and ?as. Their
subtyping rules compare required names independently of source order and allow
narrower property value types. An open expected shape allows extra properties;
a closed expected shape requires the same public property set and a closed
actual shape. Every object shape is a subtype of object. A class name alone
does not promise that its properties are initialized with the required current
values; use a shape or an intersection to express that requirement.
The opening #{ is one token. Whitespace or a comment cannot separate # from
{. Object shapes are types and patterns, not object construction expressions.
See Match and Destructuring for extracting their
properties in a match arm.
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) as int;
}
assert!(count_values(vec[1, 2]) == 2);
assert!(count_values(dict['one' => 1]) == 1);
assert!(count_values((1, 'two')) == 2);
Vec and tuple keys are uint positions starting at 0u. Nonempty vecs and
tuples can satisfy array<uint, V> but cannot satisfy array<int, V>.
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.
Where clauses
A function, method, or closure can declare upper bounds with where. The clause
follows the return type, or the parameter list when the return type is omitted:
function identity<T>(T $value): T where T: !null {
return $value;
}
$identity = fn<T>(T $value): T where T: !null => $value;
$block = fn<T>(T $value): T where T: !null {
return $value;
};
For a callable’s own type parameters, <T: Bound> and where T: Bound impose
the same requirement. Whim checks supplied or defaulted type arguments when it
binds them, including when creating a callable with explicit type arguments.
Inline bounds and where constraints both apply when a declaration uses both.
A where clause can also constrain type parameters from an enclosing scope:
class Collection<T> {
public function inspect<U>(): void where T: int|float, U: vec<T> {}
}
Each entry names an available type parameter, followed by : and its upper
bound. Instance methods can use class and method type parameters; static methods
can use only their own type parameters. Closures can use their own type parameters
and those from their enclosing scope. An inner type parameter shadows an outer
parameter with the same name.
Constraints on enclosing type parameters are checked before the body or default
parameter expressions run. Each type argument must be a subtype of its bound.
If any constraint fails, Whim throws Whim\Unwind\TypeError. Repeated constraints
must all hold.
These bounds apply only to the callable. They do not change the bounds on its
enclosing scope. For example, Collection<string> is a valid type,
but calling inspect::<vec<string>>() on it throws TypeError because string
does not fit int|float. On Collection<int>, inspect::<vec<int>>() satisfies
both bounds.
An override must accept every set of type arguments that the inherited method accepts. It may keep or weaken inherited bounds, but must not strengthen them. This applies whether a bound appears in the type parameter list or a where clause.
FunctionLikeReflection::getWhereConstraints()
exposes the function’s where constraints in source order. Reflection keeps inline
bounds in TypeParameterReflection::getBounds() and does not combine the two forms.
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 retain their newtype tags. The tag forms part of the key’s identity:
UserId(42), OrderId(42), and plain 42 can name three distinct entries.
Lookup requires the same tag and backing value. Iteration returns the tagged
key, and a dict with tagged keys can satisfy dict<UserId, string>.
JSON and BSON object names must be plain strings. Cast tagged keys to string
before encoding them; these formats cannot preserve distinct newtype keys with
the same text.
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.
Whim\Unwind\UnsupportedPlatformException extends RuntimeException and
reports an operation the running platform cannot provide. Such operations
fail before starting work. Before sending or watching a process signal, call
isSupported() on its Whim\Process\Signal value. It returns false for all
signals on Windows; unsupported signals also return false from isCatchable().
Whim\Command\UnsupportedSignalException extends this exception too.
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) evaluates its message expression, then writes the string
and current stack trace to standard error and exits with status 255. A
non-string result throws a catchable TypeError before the panic occurs.
Errors during message evaluation also propagate normally. Panic 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 as a uint. 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(): uint
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>.
Vecs and tuples yield uint keys. Dicts and custom iterators keep their key types.
$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<uint, string> {
public function __construct(private vec<string> $names) {}
public function toIterator(): Iterator<uint, string> {
return new ArrayIterator::<uint, 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.
take and drop accept uint counts; count returns a uint.
use Whim\Iterate;
$values = Iterate\take::<uint, int>(
Iterate\filter::<uint, int>(
Iterate\map::<uint, int, int>(
vec[1, 2, 3, 4],
fn(int $value): int => $value * 2,
),
fn(int $value): bool => $value > 4,
),
1u,
);
assert!(Iterate\to_vec::<uint, 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::<uint>(fn(string $value): uint => length!($value));
assert!($length->await() == 4u);
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 as a uint. 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 nonzero uint 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>(4u);
$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|uintreturnsnullfor an unbounded channel.count(): uintreturns 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 file, 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')] stores the class and arguments. Reflection’s
newInstance() creates a Table value. The arguments follow the same count
and type rules as a normal constructor call and 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_FILE | source file |
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 stores its own arguments. Source order stays intact.
File attributes
Use #![...] to attach attributes to the whole file. The opening #![ is one
token. A file attribute list is a top-level statement and needs no semicolon.
namespace App;
use Whim\Attribute\Attribute;
use Whim\Reflection;
#[Attribute(Attribute::TARGET_FILE | Attribute::IS_REPEATABLE)]
final readonly class Tag {
public function __construct(public string $name) {}
}
#![Tag(name: 'http')]
function home(): string {
return '/';
}
$file = Reflection\reflect_function('App\\home')->getFile();
foreach ($file->getAttributes::<Tag>() as $attribute) {
write_line!($attribute->newInstance()->name);
}
Put file attributes at the top level of a file or namespace, including after imports or executable statements. They apply to the whole file. The compiler collects them before execution and optimization. Function bodies and control-flow blocks contain only executable statements, so they cannot contain file attributes.
Names follow the namespace and imports at the attribute’s location. Arguments
follow the same constant-expression rules as other attributes and cannot read
local variables. All file attribute lists share one target, including lists in
different namespaces. Repeating an attribute anywhere in the file requires
IS_REPEATABLE. Reflection returns them in source order.
File attributes belong to the file, so a symbol’s getAttributes() does not
include them. Use the symbol’s getFile() to read them. A file that contains
only declarations, imports, and file attributes has no top-level code.
Reading attributes
Use Whim\Reflection to inspect attributes:
use Whim\Attribute\Attribute;
use Whim\Reflection;
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final readonly class Tag {
public function __construct(public string $name) {}
}
#[Tag('http')]
#[Tag('public')]
final class Handler {}
$handler = Reflection\reflect_class('Handler');
assert!($handler != null);
foreach ($handler->getAttributes::<Tag>() as $attribute) {
$tag = $attribute->newInstance();
write_line!($tag->name);
}
Every declaration reflection provides getAttributes::<T>() and
getAttributesByName(). Both return attribute reflections without running an
attribute constructor. AttributeReflection::newInstance() constructs a fresh
attribute value.
Use reflect_function() for a function. A class-like reflection provides its
methods, properties, and constants. A method reflection provides its
parameters. reflect_object($case)->getEnumCase() finds an enum case. Missing
declarations and members return null.
See Reflection for the complete API.
Whim rejects non-constant arguments, missing attribute classes, invalid
targets, and illegal repeats before top-level code runs. newInstance() checks
argument values against the constructor’s parameter types.
Built-in Attributes
Whim supplies attributes for call contracts, error reports, and lint settings.
Lint settings
Whim\Lint\Allow, Warn, Deny, and Forbid set a lint rule’s level within a
declaration or file. All four are repeatable and accept a string $rule and an
optional null|string $reason = null. They support every attribute target and
remain ordinary attribute objects at runtime. See Scoped lint attributes
for their scope and argument rules.
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\Process;
use Whim\Marker\Stub;
#[Stub]
function get_id(): (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 Trifle packages on Codeberg. Each package has its own repository.
These packages use names under Trifle\. Their Git tags define their
versions. Install them through Whim’s Git package manager.
| Package | Description | Install |
|---|---|---|
ansi | ANSI colours, styles, cursor movement, and screen controls. | whim add git+ssh://[email protected]/trifle/ansi |
args | Typed command-line parsing and generated usage. | whim add git+ssh://[email protected]/trifle/args |
cache | Stampede-safe local LRU and database caches. | whim add git+ssh://[email protected]/trifle/cache |
config | Typed configuration from values, files, and the environment. | whim add git+ssh://[email protected]/trifle/config |
console | Terminal output, tables, progress, and prompts. | whim add git+ssh://[email protected]/trifle/console |
cqrs | Command and query buses with events, logs, and traces. | whim add git+ssh://[email protected]/trifle/cqrs |
diff | Myers diffs for values and text, with unified output. | whim add git+ssh://[email protected]/trifle/diff |
dotenv | .env parsing and environment layering. | whim add git+ssh://[email protected]/trifle/dotenv |
event | Typed events with hierarchy-aware listeners. | whim add git+ssh://[email protected]/trifle/event |
expect | Fluent expectations for tests. | whim add git+ssh://[email protected]/trifle/expect |
fake | Deterministic fake data generated from a seed. | whim add git+ssh://[email protected]/trifle/fake |
feed | Atom and RSS feed generation. | whim add git+ssh://[email protected]/trifle/feed |
humanize | Human-readable durations, sizes, counts, and numbers. | whim add git+ssh://[email protected]/trifle/humanize |
lock | Leased mutual exclusion over pluggable stores. | whim add git+ssh://[email protected]/trifle/lock |
log | Structured logging with composable loggers. | whim add git+ssh://[email protected]/trifle/log |
markdown | CommonMark and GitHub-flavoured Markdown parsing. | whim add git+ssh://[email protected]/trifle/markdown |
markdown-ansi | Markdown rendering for styled terminal text. | whim add git+ssh://[email protected]/trifle/markdown-ansi |
markdown-html | Markdown rendering for HTML. | whim add git+ssh://[email protected]/trifle/markdown-html |
money | Integer money, ISO 4217 currencies, and lossless allocation. | whim add git+ssh://[email protected]/trifle/money |
otp | HOTP, TOTP, and authenticator provisioning. | whim add git+ssh://[email protected]/trifle/otp |
qr | QR code generation with matrix and SVG output. | whim add git+ssh://[email protected]/trifle/qr |
queue | Message queues with retries, workers, and pluggable stores. | whim add git+ssh://[email protected]/trifle/queue |
seal | Sealed payloads and signed JWTs with key rotation. | whim add git+ssh://[email protected]/trifle/seal |
semver | Semantic versions and Cargo-style requirements. | whim add git+ssh://[email protected]/trifle/semver |
template | Logic-less Mustache templates rendered from JSON values. | whim add git+ssh://[email protected]/trifle/template |
theme | Paints and themes for terminal text. | whim add git+ssh://[email protected]/trifle/theme |
trace | Timed spans recorded through structured logs. | whim add git+ssh://[email protected]/trifle/trace |
Codeberg hosts all Trifle package repositories.
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') == 4u);
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) evaluates an expression that must produce a string, prints
the same kind of stack trace, then exits with status 255. The panic cannot be
caught; a non-string message throws a catchable TypeError instead. Use panic
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\NonZero;
function repeat_name(NonEmptyString $name, NonZero<uint> $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(256u);
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|uint|bool|fresh.Numericisint|uint|float.Scalarisint|uint|float|string|bool|fresh.Nullable<T>isT|null.NonNull,NonEmptyString,NonEmptyVec<T>, andNonEmptyDict<K, V>exclude empty values.NonZero<T>excludes zero from a numeric type.NonZero<uint>accepts positive uints;NonZero<int>accepts positive and negative nonzero ints.NegativeIntandNonPositiveIntname signed integer ranges.Uint8,Uint16, andUint32name unsigned integer ranges with fixed bounds.Int8,Int16, andInt32name their signed counterparts.Percentis0u..=100u;Digitis0u..=9u.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.
Standard-library counts and lengths use uint, with literals such as 0u
and 16u. The length!() built-in also returns uint.
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|uint|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 as a uint. Normal programs seldom need to call it.
Function Helpers
Whim\Fn provides five helpers that return callables. They use the
Transform<I, O> and Consumer<T> aliases from Whim\Refine and need no
package install.
| Helper | Returned callable |
|---|---|
identity<T>() | Returns its input unchanged. |
tap<T>(callback) | Calls the callback, then returns its input. |
after<I, O, R>(first, next) | Passes the result of first to next. |
when<I, O>(condition, then, otherwise) | Tests the input and calls the chosen branch. |
rethrow() | Throws the throwable passed to it. |
The helpers do not call their callbacks until you call the returned function.
Callbacks may suspend or throw. after waits for the first callback to return
before it calls the next; when calls only the chosen branch. Errors pass
through unchanged.
Identity and observation
identity returns the same value. tap adds a callback without changing the
value passed to the next step:
use Whim\Fn;
use Whim\Reference\Strong;
$identity = Fn\identity::<int>();
assert!($identity(7) == 7);
$seen = new Strong::<int>(0);
$observe = Fn\tap::<int>(fn(int $value): void {
$seen->value = $value;
});
assert!($observe(3) == 3);
assert!($seen->value == 3);
tap returns the input after the callback runs. A callback can still change an
object it receives, as with any other function call.
Composition and branches
after runs two transforms in order. The first output type must match the
second input type:
use Whim\Fn;
$doubleThenText = Fn\after::<int, int, string>(
fn(int $value): int => $value * 2,
fn(int $value): string => 'n=' . $value,
);
assert!($doubleThenText(4) == 'n=8');
when passes the same input to the predicate and the chosen transform. Both
branches must return the declared output type:
use Whim\Fn;
$choose = Fn\when::<int, string>(
fn(int $value): bool => $value > 0,
fn(int $_): string => 'positive',
fn(int $_): string => 'other',
);
assert!($choose(1) == 'positive');
assert!($choose(0) == 'other');
Rethrowing errors
rethrow returns a fn(Throwable): never. Use it when an error callback should
throw the error it receives:
use Whim\Fn;
use Whim\Unwind\RuntimeException;
$rethrow = Fn\rethrow();
$expected = new RuntimeException('failed');
$caught = null;
try {
$rethrow($expected);
} catch (RuntimeException $error) {
$caught = $error;
}
assert!($caught == $expected);
Each helper has #[MustUse]: store, pass, call, or explicitly discard the
returned callable.
Reflection
Whim\Reflection inspects loaded files, declarations, types, and values.
Files
get_loaded_files() returns loaded FileReflection values, sorted by path
with anonymous inputs first. reflect_file($path) finds a loaded file or
returns null. Both functions read loaded metadata without loading or
executing a file.
A file reflection provides:
getPath(): the recorded source path, ornullfor an anonymous input such as standard input.getOrigin(): the file’sDeclarationOrigin, matching its symbols’ origin.getLocation(): the whole source span for a user file with a path, ornullotherwise.getAttributes::<T>()andgetAttributesByName($class): file attributes in source order. Each attribute’sgetTarget()returns itsFileReflection.getDocumentation():null; files have no declaration documentation.getSymbols($kind = null): the file’s named symbols, sorted by fully qualified name. Pass aWhim\Symbol\SymbolKindto select one kind.hasTopLevelCode(): whether the source contains executable statements outside function and method bodies, including statements inside namespaces.
use Whim\Reflection;
use Whim\Symbol\SymbolKind;
foreach (Reflection\get_loaded_files() as $file) {
write_line!($file->getPath() ?? '<anonymous>');
foreach ($file->getSymbols(SymbolKind::Function) as $function) {
write_line!(' ' . $function->getName());
}
}
Lookup matches the recorded path exactly, including its bytes. Use a path from
getPath() or SourceLocation::getFile(). Artifacts keep their individual
source files; the artifact container itself is not an extra source file. If a
path has been loaded more than once, lookup and listing use its latest load.
Anonymous inputs remain separate entries even when they share a diagnostic
label.
Every symbol reflection has getFile(), which returns its defining
FileReflection or null for a symbol defined in Rust. This works for classes,
interfaces, enums, functions, constants, type aliases, and newtypes, including
symbols from anonymous inputs. A symbol keeps its defining file even when the
same path is loaded again later.
Symbols include declarations across all namespaces in the file. Methods,
closures, imports, and stub declarations backed by another definition are not
separate symbols in this list. Files without named symbols still appear in
get_loaded_files().
The compiler records hasTopLevelCode() before optimization. Unreachable
statements still count, even if optimization removes them. Imports,
declarations, and file attributes do not count. Initializers
within declarations do not count either, so this flag does not guarantee that
loading the file has no side effects.
FileReflection implements DeclarationReflection. The
attribute syntax for files is
#![...]; the target flag is Attribute::TARGET_FILE.
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.
Symbols and members
The Whim\Reflection 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.
Invoke callables and create instances
FunctionReflection::invoke($typeArguments = vec[], $arguments = vec[]) calls
the reflected function. CallableReflection::invoke() takes the same
arguments and calls the fn value retained by reflect_callable(). Both
implement Whim\Reflection\InvokableReflection.
Type arguments are a vec<TypeReflection> in type parameter order. Value
arguments are a vec<mixed> in parameter order. Omitted arguments use the
callable’s defaults. Whim checks argument counts, types, bounds, where
constraints, and return types as it does for a direct call. It does not infer
type arguments from values.
use Whim\Reflection;
function identity<T>(T $value): T {
return $value;
}
$function = Reflection\reflect_function('identity');
$int = Reflection\reflect_type::<int>();
assert!($function->invoke(vec[$int], vec[42]) == 42);
$prefix = 'Hello, ';
$greet = Reflection\reflect_callable(fn(string $name): string => $prefix . $name);
assert!($greet->invoke(vec[], vec['world']) == 'Hello, world');
A callable keeps its captures, bound receiver, class scope, type
bindings, and partial arguments. Pass only the remaining arguments to a
partial application. A specialized fn uses its stored type arguments; passing
more type arguments throws TypeError. ClosureReflection
describes a closure declaration. To invoke a closure with its captures,
reflect the fn value with reflect_callable().
MethodReflection::invoke($target, $typeArguments = vec[], $arguments = vec[])
requires an object for an instance method and a classname<object> for a
static method. The target’s class must be the declaring class or one of its
subtypes. An unrelated class fails even if it has a method with the same name.
The call uses the target’s override when one exists. For a static call, the
target class binds static, including new static() and a static return
type. The method’s declaring class still determines self and parent.
Visibility uses the scope that calls invoke(). Holding a reflection of a
private or protected method grants no extra access. Instance methods get
class type arguments from the object; the supplied type arguments bind only
the method’s own type parameters.
ClassReflection::instantiate($typeArguments = vec[], $arguments = vec[])
creates an object and runs its constructor. Type arguments bind the class;
value arguments go to the constructor. Normal constructor visibility and
instantiation rules apply. Constructors and destructors cannot be called
through MethodReflection::invoke().
Every supplied TypeReflection must be resolved. Use resolve() with a type
environment to replace unbound parameters before passing them. Invocation
returns the callable’s result, or null for a void callable. Exceptions
propagate to the caller, and calls may suspend as usual. Discarding an invoked
callable’s result still enforces its #[MustUse] attribute.
Generics
A generic declaration lists its type parameters in source order. Each
TypeParameterReflection gives its owner, position, variance, bounds, and
default.
FunctionLikeReflection::getWhereConstraints() returns
Whim\Reflection\Generic\WhereConstraintReflection entries in source order,
including repeated constraints. Functions, methods, and closures without a clause
return an empty vector.
Each constraint provides:
getParameter(): the exactTypeParameterReflectionit constrains.getBound(): its upper bound as aTypeReflection, with generic parameters left unresolved.getDeclaringFunctionLike(): the function, method, or closure that declares the constraint.getPosition(): its zero-based position in the clause.getLocation(): its source span for user code, ornullfor an artifact.
Inherited methods retain their original declaring method and type parameters.
Resolve a bound with an object or callable’s TypeEnvironmentReflection to
substitute its type arguments. Where constraints remain separate from
TypeParameterReflection::getBounds(), which describes the parameter declaration.
Artifacts preserve these constraints.
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.
ParameterReflection::getDefaultValue($environment = null) evaluates a
parameter’s default with the supplied type bindings. Pass the callable’s type
environment for a generic function or method, or the object’s or specialized
class type’s environment for a constructor parameter. Each call evaluates the
default again, including any object or closure it creates.
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, string lengths, named types, unions,
intersections, negation, functions, collections, shapes, class names, tuples,
wildcards, type parameters, and static.
The primitive fresh uses PrimitiveTypeReflection, like int and uint.
Its kind is TypeKind::Fresh:
use Whim\Reflection;
use Whim\Reflection\Type\PrimitiveTypeReflection;
use Whim\Reflection\Type\TypeKind;
use Whim\Type;
$identity = fresh!();
$type = Reflection\reflect_type::<fresh>();
assert!($type is PrimitiveTypeReflection);
assert!($type->getKind() == TypeKind::Fresh);
assert!($type->toString() == 'fresh');
assert!($type->accepts($identity));
assert!(!$type->accepts($identity as uint));
assert!($type->equals(Reflection\reflect_type_of($identity)));
assert!($type->getId() == Type\of($identity));
assert!(Reflection\reflect_type_id($type->getId())?->equals($type));
Every fresh identity has the same type ID. That ID identifies the fresh
type; $identity as uint gives the value’s representation. Reflection keeps
fresh in collection types, generic arguments, properties, and callable
signatures. A fresh-backed newtype keeps its tag; reflecting its
getBackingValue() returns the fresh type.
The primitive uint has kind TypeKind::Uint. Literal, integer-range, enum
backing-value, and dict-shape-key reflection preserve unsigned values and their
full 64-bit magnitude. Their value and bound results can therefore be uint.
StringLengthTypeReflection reports the least byte length and the optional
greatest byte length.
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() returns a CallableReflection for a fn value. It reports
the declaration, function type, type bindings, bound object, called class,
captured values, and bound arguments. Its getDeclaration() returns a
FunctionReflection, MethodReflection, or ClosureReflection. These share
the FunctionLikeReflection interface for declaration metadata.
Three predicates distinguish callable values:
| Value | isClosure() | isFirstClassCallable() | isPartialFunctionApplication() |
|---|---|---|---|
fn(int $x): int => $x | true | false | false |
foo(...) or $object->method(...) | false | true | false |
foo::<int>(...) | false | true | false |
foo(?, 'fixed') or foo(?, ?) | false | false | true |
Exactly one predicate returns true. Partial function application reports
isPartialFunctionApplication() even when it binds no values or starts from a
closure. getDeclaration() still reports the original declaration. Binding
type arguments or using (...) on an existing callable preserves its kind.
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.
Object shape types
reflect_type::<#{ value: int, ... }>() returns an
ObjectShapeTypeReflection with kind TypeKind::ObjectShape. isOpen() reports
whether extra public instance properties are allowed. getProperties() returns
ObjectShapePropertyReflection values in source order; each has getName() and
getType() methods. The usual accepts(), isSubtypeOf(), resolve(), and type
identity operations apply to object shapes. accepts() checks current property
values on every call.
Strings, Numbers, and Binary Data
Byte strings
Whim strings hold bytes, so Whim\Str uses byte offsets and byte
lengths.
length returns uint; searches return null|uint. Search and slice input
offsets stay signed, so negative offsets can count from the end. Slice,
splice, and comparison lengths use null|uint. A length larger than the
remaining bytes clamps to the end. Repeat counts and padding lengths use
uint too. Chunk sizes, split limits, and wrap widths use NonZero<uint>.
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, 3u) == 'bcd');
assert!(Str\length('abcdef') == 6u);
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), UInt\try_parse($text), and Float\try_parse($text)
return null for invalid input. They do not accept a partial number.
UInt\try_parse accepts decimal digits, an optional leading +, and leading
zeros. It rejects a minus sign, whitespace, underscores, base prefixes, source
suffixes, fractions, exponents, and values above the unsigned maximum.
UInt\div(uint $numerator, uint $denominator): uint returns the exact integer
quotient and throws DivisionByZeroError for a zero denominator.
use Whim\Math;
use Whim\UInt;
assert!(UInt\try_parse('18446744073709551615') == Math\UINT_MAX);
assert!(UInt\try_parse('42u') == null);
assert!(UInt\div(Math\UINT_MAX, 2u) == 9_223_372_036_854_775_807u);
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.
expm1(x) computes exp(x) - 1, and log1p(x) computes ln(1 + x).
Both keep accuracy when x is close to zero. log1p(-1.0) returns negative
infinity, and inputs below -1.0 return NAN. tanh computes the hyperbolic
tangent. All three use native floating-point operations and preserve signed zero.
dot multiplies pairs of floats and adds the products in iteration order.
It accepts vectors, tuples, dictionaries, and objects that implement Iterator
or ToIterator. Dictionary keys do not affect the pairs. Both inputs must have
the same length, or it throws Whim\Unwind\LengthException. Empty inputs return
0.0.
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.
to_base accepts non-negative ints and the full uint range. Numeric comparison
and aggregate helpers such as min, max, mean, and median accept uints.
sum keeps its signed inputs; float-only functions still require float.
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.
UINT_MIN and UINT_MAX are 0u and 18_446_744_073_709_551_615u.
Whim\Refine\Uint8, Uint16, and Uint32 accept uints within their bit
widths: 0u..=255u, 0u..=65_535u, and 0u..=4_294_967_295u.
NonZero<uint> excludes 0u. These types do not accept signed ints.
Use uint for the full unsigned 64-bit range.
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().
Unsigned decoders and reader methods always return uint, including for zero
and small values. Cast the result to int when a signed API requires it;
values above INT_MAX fail that cast.
Unsigned encoders and writer methods require uints within the chosen width,
including the 64-bit methods. All reject values that do not fit.
Reader byte counts, skips, cursors, lengths, and remaining-byte counts also
use uint.
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.
fill and reproduce take a uint size. The reproduce callback receives a
uint index.
keys and key-aware callbacks use uint keys for vecs and tuples.
enumerate returns (uint, T) pairs.
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.
take, drop, and slice take uint counts and offsets. chunk takes a
nonzero uint size.
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.
count_values returns uint counts.
Copying a vec or tuple into a dict keeps its uint keys; 0 and 0u remain
distinct dict keys.
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.
As with vecs, take, drop, and slice take uint counts and offsets.
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. count() returns a uint.
These containers yield uint keys during iteration.
The heap and priority queue do not share an implementation. Equal values or priorities have no set relative order.
Graphs
Whim\Graph provides directed and undirected graphs, traversal, cycle checks,
topological sorting, and shortest paths. Graphs store nodes of type TNode
and edge weights of type TWeight. They compare nodes with Whim equality;
nodes do not need an ordering or a dictionary key type.
Building a graph
Use directed() or undirected() to create an empty graph. add_node() and
add_edge() return a graph with the added node or edge. The original graph
keeps its nodes and edges. add_edge() adds either endpoint if it is missing:
use Whim\Graph;
$empty = Graph\directed::<string, int>();
$graph = Graph\add_edge::<string, int>($empty, 'A', 'B', 1);
$graph = Graph\add_edge::<string, int>($graph, 'A', 'C', 5);
$graph = Graph\add_edge::<string, int>($graph, 'B', 'C', 1);
assert!(Graph\nodes::<string, int>($empty) == vec[]);
assert!(Graph\nodes::<string, int>($graph) == vec['A', 'B', 'C']);
assert!(Graph\neighbors::<string, int>($graph, 'A') == vec['B', 'C']);
assert!(Graph\has_path::<string, int>($graph, 'A', 'C'));
assert!(!Graph\has_path::<string, int>($graph, 'C', 'A'));
assert!(Graph\bfs::<string, int>($graph, 'A') == vec['A', 'B', 'C']);
assert!(Graph\topological_sort::<string, int>($graph) == vec['A', 'B', 'C']);
assert!(Graph\shortest_path::<string>($graph, 'A', 'C') == vec['A', 'B', 'C']);
DirectedGraph<TNode, TWeight> and UndirectedGraph<TNode, TWeight> implement
the sealed Graph<TNode, TWeight> interface. Both classes are readonly.
The graph protects its structure; node values and weights may still hold
mutable objects.
Both classes provide these methods:
| Method | Result |
|---|---|
getNodes() | All nodes in insertion order. |
getEdgesFrom(node) | Outgoing Edge<TNode, TWeight> objects in insertion order. |
hasNode(node) | Whether the node exists. |
hasEdge(from, to) | Whether an edge connects the nodes. |
withNode(node) | A graph containing the node. |
withEdge(from, edge) | A graph containing the edge; both endpoints must already exist. |
hasCycle() | Whether the graph contains a cycle. |
Edge<TNode, TWeight> holds a public readonly to node and a
null|TWeight weight. Its weight defaults to null.
You can also pass a node vector and a vec<(TNode, Edge<TNode, TWeight>)> to
either graph constructor. A constructor throws
Whim\Unwind\InvalidArgumentException if an edge refers to a missing node.
An undirected graph adds reverse edges with the same weights.
Traversal and ordering
| Function | Result |
|---|---|
nodes(graph) | The node vector. |
neighbors(graph, node) | Direct neighbors in edge order. |
has_path(graph, from, to) | Whether a path connects two existing nodes. |
has_cycle(graph) | Whether a cycle exists. |
bfs(graph, start) | Nodes reached in breadth-first order. |
dfs(graph, start) | Nodes reached in depth-first order. |
topological_sort(directedGraph) | A node order in which each edge’s source precedes its destination, or null for a cycle. |
Traversals visit each reached node once and follow edges in insertion order.
They return an empty vector when the start node is missing. has_path() returns
false when either endpoint is missing; an existing node has a path to itself.
Topological sorting returns an empty vector for an empty graph.
Shortest paths
shortest_path() accepts a graph with integer weights. shortest_path_by()
accepts any weight type and a callback that converts each weight to an integer
cost. Both return a nonempty vector from source to destination, or null if
an endpoint is missing or the destination is unreachable. An edge with a
null weight costs 1; the conversion callback does not run for that edge.
use Whim\Graph;
$graph = Graph\directed::<string, string>();
$graph = Graph\add_edge::<string, string>($graph, 'A', 'B', 'long');
$graph = Graph\add_edge::<string, string>($graph, 'A', 'C', 'x');
$graph = Graph\add_edge::<string, string>($graph, 'C', 'B', 'x');
$path = Graph\shortest_path_by::<string, string>(
$graph,
'A',
'B',
fn(string $weight): int => length!($weight) as int,
);
assert!($path == vec['A', 'C', 'B']);
The search uses Dijkstra’s algorithm for nonnegative costs and Bellman-Ford
when any cost is negative. It throws NegativeCycleException when a negative
cycle is reachable from the source and can reach the destination. A cycle
elsewhere does not make that path fail. A path cost outside the integer range
throws Whim\Unwind\OverflowError.
Trees
Whim\Tree provides typed trees, traversal, search, mapping, filtering, and
folds. Each Node<T> holds a value and an ordered vector of children. The
sealed interface has two readonly classes: TreeNode<T> and LeafNode<T>.
Building a tree
Use tree() for a value with children and leaf() for a value with no
children. Both classes expose getValue() and getChildren():
use Whim\Tree;
$tree = Tree\tree::<string>('root', vec[
Tree\leaf::<string>('left'),
Tree\tree::<string>('right', vec[Tree\leaf::<string>('child')]),
]);
assert!(Tree\count::<string>($tree) == 4u);
assert!(Tree\depth::<string>($tree) == 2u);
assert!(Tree\pre_order::<string>($tree) == vec['root', 'left', 'right', 'child']);
assert!(Tree\post_order::<string>($tree) == vec['left', 'child', 'right', 'root']);
assert!(Tree\leaves::<string>($tree) == vec['left', 'child']);
assert!(Tree\path_to::<string>($tree,
fn(string $value): bool => $value == 'child',
) == vec['root', 'right', 'child']);
assert!(Tree\at_index::<string>($tree, vec[1u, 0u])->unwrap() == 'child');
You can also construct TreeNode<T>(value, children) or LeafNode<T>(value)
directly. A TreeNode with no children counts as a leaf. Tree shape stays
fixed; a stored value may still hold a mutable object.
Traversal and search
| Function | Result |
|---|---|
is_leaf(node) | Whether the node has no children. |
count(node) | The number of nodes, including the root. |
depth(node) | The greatest depth; a root with no children has depth 0u. |
leaves(node) | Leaf values, in child order. |
pre_order(node) | Each value before its children’s values. |
post_order(node) | Each value after its children’s values. |
level_order(node) | Values in breadth-first order. |
all(node, predicate) | Whether every value matches. |
any(node, predicate) | Whether any value matches. |
contains(node, value) | Whether a value compares equal under Whim equality. |
find(node, predicate) | The first breadth-first match as Some<T>, or None. |
path_to(node, predicate) | The value path to the first breadth-first match, or null. |
at_index(node, indexPath) | The value at a path of unsigned child indices as Some<T>, or None. |
to_index(node, predicate) | The child-index path to the first breadth-first match, or null. |
all() and any() call predicates in pre-order and stop when the answer is
known. Value paths include the root. An empty index path refers to the root;
to_index() returns an empty vector when the root matches. at_index()
accepts an iterable of uint indices, including a vector, tuple, or iterator.
Transforming and folding
| Function | Result |
|---|---|
reduce(node, callback, initial) | An accumulator built in pre-order; the callback receives (accumulator, value). |
map(node, callback) | A tree with the same shape and transformed values. |
filter(node, predicate) | A tree of matching nodes whose ancestors also match, or null if the root fails. |
fold(node, callback) | A result built from child results; the callback receives (value, vec<childResult>). |
traverse(node, callback) | A result with lazy child results; the callback receives (value, fn(): vec<childResult>). |
to_dict(node) | A nested dictionary with value and children entries. |
map() and fold() process children before their parent. filter() tests a
node before its children; rejecting a node drops its whole subtree. It returns
TreeNode<T> objects for all retained nodes, including leaves.
traverse() lets a callback skip children. Calling its child supplier computes
and caches that node’s child results. Later calls return the cached results:
use Whim\Math;
use Whim\Tree;
$tree = Tree\tree::<int>(1, vec[Tree\leaf::<int>(2), Tree\leaf::<int>(3)]);
$total = Tree\traverse::<int, int>(
$tree,
fn(int $value, fn(): vec<int> $children): int => $value + Math\sum($children()),
);
assert!($total == 6);
$root = Tree\traverse::<int, int>(
$tree,
fn(int $value, fn(): vec<int> $_children): int => $value,
);
assert!($root == 1);
Building from flat data
from_iterable<TItem, TId, TValue>() accepts an iterable of items and three
callbacks: an item ID, its parent ID as Option<TId>, and its stored value.
TId must satisfy Whim\Refine\ArrayKey. None marks the root:
use Whim\Option\None;
use Whim\Option\Option;
use Whim\Option\Some;
use Whim\Tree;
type Row = (string, null|string);
$tree = Tree\from_iterable::<Row, string, string>(
vec[('child', 'root'), ('root', null)],
fn(Row $row): string => $row[0],
fn(Row $row): Option<string> {
if ($row[1] == null) {
return new None();
}
return new Some::<string>($row[1]);
},
fn(Row $row): string => $row[0],
);
assert!(Tree\pre_order::<string>($tree) == vec['root', 'child']);
Items may appear before their parents. Children follow input order. The builder reads the iterable once, then checks the IDs and parent paths before it builds the tree. Use stable callbacks: the builder may call them more than once.
All input errors extend Whim\Unwind\InvalidArgumentException:
| Error | Cause |
|---|---|
NoRootNodeException | No item has None as its parent, including empty input. |
MultipleRootNodesException | More than one item has None as its parent. |
OrphanedNodeException | An ID repeats, a parent is missing, or a parent chain forms a cycle. |
Variation
Whim\Variation holds values that depend on named choices. A fresh identity
names a dimension. Every choice with that identity follows the same selection:
false takes its first branch; true takes its second.
use Whim\Variation;
$dimension = fresh!();
$x = Variation\choose::<int>($dimension, 10, 20);
$y = Variation\choose::<int>($dimension, 1, 2);
$sum = Variation\combine::<int, int, int>(
$x,
$y,
fn(int $a, int $b): int => $a + $b,
);
assert!($sum->select(dict[$dimension => false]) == 11);
assert!($sum->select(dict[$dimension => true]) == 22);
The sum has two outcomes, 11 and 22. It cannot mix the first branch of $x
with the second branch of $y. To make two choices independent, give them
different fresh identities.
Building choices
The sealed interface Variation<T> has two forms:
Value<T>holds a plain value in its readonlyvalueproperty.Choice<T>holds a readonlydimension,whenFalse, andwhenTrue. Both branches areVariation<T>values, so choices can nest and share branches.
value::<T>($value) creates a Value<T>.
choose::<T>($dimension, $whenFalse, $whenTrue) creates a Choice<T> from
two plain values. Use the Choice<T> constructor to join existing trees:
use Whim\Variation;
use Whim\Variation\Choice;
$size = fresh!();
$colour = fresh!();
$small = Variation\choose::<string>($colour, 'small red', 'small blue');
$large = Variation\choose::<string>($colour, 'large red', 'large blue');
$item = new Choice::<string>($size, $small, $large);
assert!($item->select(dict[$size => true, $colour => false]) == 'large red');
A repeated dimension stays linked even if it appears at different depths or
in a different order in another tree. Choices do not impose an order on
fresh values.
Selecting and specializing
select(dict<fresh, bool> $selection): T follows one path and returns its value.
It throws Unwind\InvalidArgumentException if a dimension on that path has no
selection. Extra selections do no harm. A dimension that occurs only on a
branch the method does not visit needs no selection.
specialize($selection): Variation<T> resolves the supplied dimensions and
keeps the remaining choices. It returns a new tree when needed and shares
unchanged branches with the source. Supplying every dimension produces a
Value<T>:
use Whim\Variation;
$dimension = fresh!();
$number = Variation\choose::<int>($dimension, 10, 20);
$chosen = $number->specialize(dict[$dimension => true]);
assert!($chosen is Variation\Value<int>);
assert!($chosen->select(dict[]) == 20);
assert!($number->select(dict[$dimension => false]) == 10);
Mapping and combining
map::<U>(fn(T): U $function): Variation<U> transforms each reachable leaf.
combine::<A, B, C>($left, $right, fn(A, B): C $function): Variation<C>
transforms each reachable pair. Both call the function during the operation.
Later calls to select() do not run it again. A thrown error stops the operation
and reaches the caller.
Callbacks should be free of side effects: the operation may call them for several alternatives, including repeated values. Independent dimensions can make the number of pairs grow quickly. These operations keep a tree of choices; they do not build a list of every selection.
The tree is readonly. A value stored inside it can still be a mutable object; the library does not clone or freeze that object. Arithmetic, branching, and other operations use explicit callbacks rather than new language operators.
See Logic Variables for shared values that become bound later,
and Built-in Values for fresh identities.
Logic Variables
Whim\Logic\Variable<T> is a shared variable that starts unbound. Binding it
gives every reader a value. Linking two variables means that binding either
also binds the other:
use Whim\Logic;
$x = new Logic\Variable::<int>();
$y = new Logic\Variable::<int>();
Logic\unify::<int>($x, $y);
$y->bind(42);
assert!($x->get() == 42);
assert!($y->get() == 42);
Assigning a variable object to another local keeps the same shared state.
Variable<T> is mutable; it is not a copy value.
Binding and linking
bind(T $value): void sets the binding. Binding it again succeeds only if the
new value compares equal with ==; otherwise it throws ConflictException.
That exception extends Unwind\LogicException.
Equality follows Whim’s usual rules. Objects compare by identity. NaN does not equal itself, so binding an already bound NaN again throws. A binding does not freeze a mutable object stored as its value.
unify::<T>($left, $right): void links two variables. The instance method
$left->unify($right) does the same work. Both variables must have the same
value type T.
- Two unbound variables share their next binding.
- Linking an unbound variable to a bound one gives it that binding at once.
- Two bound variables can link when their values compare equal. They then share one of those values; the library does not promise which copy it keeps.
- Unequal bindings throw
ConflictExceptionand keep their separate bindings. - Linking a variable to itself, or linking already linked variables, has no further effect.
bind() and unify() do not suspend. A later binding or link wakes readers
already waiting on either variable. Chains and repeated links cannot form a
cycle.
isBound(): bool reports whether a variable has a binding. A bound null is
distinct from an unbound variable:
use Whim\Logic\Variable;
$value = new Variable::<null>();
assert!(!$value->isBound());
$value->bind(null);
assert!($value->isBound());
assert!($value->get() == null);
Waiting and cancellation
get(null|Async\CancellationToken $cancellation = null): T returns a bound
value at once, or suspends the current task until a binding arrives:
use Whim\Async;
use Whim\Logic\Variable;
$value = new Variable::<string>();
$reader = Async\spawn::<string>(fn(): string => $value->get());
Async\later();
$value->bind('ready');
assert!($reader->await() == 'ready');
Cancellation stops only that read. It leaves the variable, its binding, and
other readers intact. A token that has already cancelled makes get() throw
even if the variable is bound. An unbound read with no future binding waits
until cancellation or task shutdown.
This library links whole variables and compares bound values. It does not
unify fields inside data structures, undo bindings, or search through possible
answers. Reads remain explicit calls to get().
See Channels and Cancellation for cancellation tokens and Variation for values with explicit alternatives.
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.
Use Clocks and Sleepers when code needs a clock or sleeper that
tests can control. Whim\Clock provides system, frozen, and offset clocks,
plus system and virtual sleepers.
Use Cron Expressions to find scheduled times in a chosen timezone.
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.
Clocks and Sleepers
Whim\Clock lets code read time and sleep through interfaces. Use the system
implementations for real time, or a controlled clock and sleeper in tests.
The types are part of the standard library; they need no package or autoloader.
Reading time
Clock::now() returns a Whim\Time\SystemTime.
Clock::nowIn($timezone) reads the clock once and returns a
Whim\DateTime\ZonedDateTime in that timezone.
SystemClockreadsSystemTime::now()on each call.FrozenClockholds a time untiladvance($duration)ortravelTo($time)changes it. A negative duration moves it backward.OffsetClockadds a fixed duration to another clock’s current reading. It follows changes to the inner clock.
Pass a Clock to code that needs the current time:
use Whim\Clock\Clock;
use Whim\Clock\FrozenClock;
use Whim\Time\Duration;
use Whim\Time\SystemTime;
final readonly class Expiry {
public function __construct(private Clock $clock) {}
public function after(Duration $duration): SystemTime {
return $this->clock->now()->plus($duration);
}
}
$clock = FrozenClock::atUnixTimestamp(1_000);
$expiry = new Expiry($clock);
assert!($expiry->after(Duration::fromMinutes(30))->getSeconds() == 2_800);
$clock->advance(Duration::fromMinutes(1));
assert!($expiry->after(Duration::fromMinutes(30))->getSeconds() == 2_860);
new FrozenClock($time) accepts an existing SystemTime.
FrozenClock::atUnixTimestamp($seconds, $nanoseconds = 0) accepts a Unix
timestamp; nanoseconds must be in 0..999_999_999.
FrozenClock::startingNow() freezes the time when the call runs.
Copies of the clock share its state. A saved reading stays unchanged when the
clock moves.
use Whim\Clock\FrozenClock;
use Whim\Clock\OffsetClock;
use Whim\DateTime\TimeZone;
use Whim\Time\Duration;
$base = FrozenClock::atUnixTimestamp(0);
$ahead = new OffsetClock($base, Duration::fromHours(1));
assert!($ahead->now()->getSeconds() == 3_600);
assert!(1 == $ahead->nowIn(TimeZone::utc())->dateTime->time->hour);
$base->advance(Duration::second());
assert!($ahead->now()->getSeconds() == 3_601);
SystemClock also implements Whim\Default\Default.
SystemClock::default() returns a new system clock.
Sleeping
Sleeper::sleep($duration, $cancellation = null) pauses the current task.
Both implementations accept a Whim\Async\CancellationToken and throw
Whim\Async\CancelledException when cancellation is requested.
SystemSleepercallsWhim\Async\sleep(). Other tasks can run while it waits. It also implementsWhim\Default\Default;SystemSleeper::default()returns a new system sleeper.VirtualSleeperadvances a linkedFrozenClockfor a positive duration, then yields once withWhim\Async\later(). It adds no real delay.
Zero and negative durations add no delay and do not move the frozen clock. Every virtual sleep still yields once, including those durations.
use Whim\Clock\FrozenClock;
use Whim\Clock\VirtualSleeper;
use Whim\Time\Duration;
$clock = FrozenClock::atUnixTimestamp(0);
$sleeper = new VirtualSleeper($clock);
$sleeper->sleep(Duration::fromDays(1));
assert!($clock->now()->getSeconds() == 86_400);
$sleeper->sleep(Duration::fromSeconds(-1));
assert!($clock->now()->getSeconds() == 86_400);
VirtualSleeper checks cancellation before advancing and after yielding.
Cancellation before the call changes no time. Cancellation during the yield
leaves any time already added in place.
Concurrent virtual sleeps each add their duration to the same clock. Two sleeps of two and three seconds advance it by five seconds. This helper does not simulate timer deadlines or advance the scheduler’s clock.
See Time and Calendars for time values and timezone operations, and Tasks and Futures for scheduling and cancellation.
Retry Policies, Circuit Breakers, and Rate Limits use these clocks and sleepers to control time in tests.
Cron Expressions
Whim\Cron\Expression parses five-field cron expressions, checks whether a
time matches, and finds the next or previous occurrence in a chosen timezone.
It computes times; your application decides how to wait for them and run work.
Parsing and matching
Expression::parse() returns an expression or null for invalid syntax.
Expression::from() throws Whim\Unwind\InvalidArgumentException on invalid
syntax. Expressions are readonly and implement Whim\Convert\ToString.
toString() returns the input with outer whitespace removed; it keeps macros,
letter case, and spacing between fields.
use Whim\Cron\Expression;
use Whim\DateTime\TimeZone;
use Whim\DateTime\ZonedDateTime;
$schedule = Expression::from('*/15 9-17 * * MON-FRI');
$zone = TimeZone::utc();
$time = ZonedDateTime::from('2026-08-21T10:15:30+00:00[UTC]')->systemTime;
assert!($schedule->matches($time, $zone));
assert!($schedule->toString() == '*/15 9-17 * * MON-FRI');
assert!(Expression::parse('60 * * * *') == null);
$next = $schedule->next($time, $zone);
assert!($zone->at($next)->dateTime->toString() == '2026-08-21T10:30:00');
$previous = $schedule->previous($time, $zone);
assert!($zone->at($previous)->dateTime->toString() == '2026-08-21T10:15:00');
| Method | Result |
|---|---|
parse(expression) | An Expression, or null for invalid syntax. |
from(expression) | An Expression, or an InvalidArgumentException. |
matches(time, zone) | Whether the minute containing the SystemTime matches. |
next(after, zone) | The first occurrence strictly after the supplied time. |
previous(before, zone) | The last occurrence strictly before the supplied time. |
toString() | The expression as written, with outer whitespace removed. |
Occurrences fall on minute boundaries. next() skips an occurrence exactly at
the supplied time. previous() also skips an exact occurrence, but can return
the start of the same minute when the supplied time includes seconds or
nanoseconds.
Fields and syntax
The fields are minute, hour, day of month, month, and day of week:
| Field | Values |
|---|---|
| Minute | 0–59 |
| Hour | 0–23 |
| Day of month | 1–31 |
| Month | 1–12, or JAN–DEC |
| Day of week | 0–7, or SUN–SAT; both 0 and 7 mean Sunday. |
Names ignore case. Separate fields with spaces or tabs. Each field accepts:
*for every value.- Lists such as
1,15,30. - Inclusive ranges such as
9-17orMON-FRI. - Positive steps such as
*/15,1-30/7, or5/17. A single value with a step runs from that value to the field’s maximum;5/17selects minutes5,22,39, and56.
A step applies within its field. It does not measure elapsed time across hours, days, or timezone changes. The parser rejects descending ranges, zero or negative steps, unknown names, and values outside the field’s range.
Day matching
When both day fields are restricted, a date matches either field. For example,
0 0 13 * FRI runs at midnight on every Friday and every thirteenth day of
the month.
A day field is unrestricted only when it is exactly *. A stepped field such
as */2 counts as restricted. When one day field is unrestricted, the other
field alone selects the days. When both are unrestricted, every day matches.
The minute, hour, and month fields must always match.
Macros
Macros ignore case and expand to these five-field forms:
| Macro | Expression |
|---|---|
@yearly, @annually | 0 0 1 1 * |
@monthly | 0 0 1 * * |
@weekly | 0 0 * * 0 |
@daily, @midnight | 0 0 * * * |
@hourly | 0 * * * * |
The parser accepts five fields and these macros. It does not accept a seconds
field, @reboot, ?, L, W, or #.
Timezones and clock changes
Every query takes a Whim\DateTime\TimeZone. The same expression can serve
more than one zone; it does not store a zone or use the system zone by default.
When clocks move backward, a local minute can occur twice. Both instants participate in matching and in next or previous searches:
use Whim\Cron\Expression;
use Whim\DateTime\TimeZone;
use Whim\DateTime\ZonedDateTime;
$schedule = Expression::from('30 2 * * *');
$paris = TimeZone::from('Europe/Paris');
$first = ZonedDateTime::from('2026-10-25T00:30:00+00:00[UTC]')->systemTime;
$second = ZonedDateTime::from('2026-10-25T01:30:00+00:00[UTC]')->systemTime;
assert!($schedule->matches($first, $paris));
assert!($schedule->matches($second, $paris));
assert!($schedule->next($first, $paris)->equals($second));
assert!($schedule->previous($second, $paris)->equals($first));
When clocks move forward, a missing local time shifts forward by the size of
the gap. In Paris, a scheduled 02:30 becomes 03:30 on the spring change.
That shifted occurrence also matches matches(). This rule handles clock
changes of other sizes, including half an hour.
Expressions that cannot occur
Valid syntax does not guarantee a calendar occurrence. For example,
0 0 30 2 * asks for February 30. next() searches through the calendar year
five years after the supplied time. previous() searches back through the year
five years before it. Both throw Whim\Cron\UnmatchableExpressionException
when they find no occurrence within that limit.
The search also follows the range supported by Time and Calendars.
Retry Policies
Whim\Retry runs a callable again after a failure. A Policy sets the maximum
attempt count, delays, jitter, error condition, and optional deadline.
use Whim\Reference\Strong;
use Whim\Retry;
use Whim\Retry\Policy;
use Whim\Unwind\RuntimeException;
$calls = new Strong::<int>(0);
$result = Retry\retry::<string>(Policy::attempts(3u), fn(): string {
$calls->value++;
if ($calls->value < 3) {
throw new RuntimeException('try again');
}
return 'done';
});
assert!($result == 'done');
assert!($calls->value == 3);
The count includes the first attempt. Policy::attempts(1u) runs once.
By default, a policy has no delay and retries only Whim\Unwind\Exception
and its subclasses. It does not retry an Error. When retries stop, the
retrier throws the last throwable unchanged.
Delays and conditions
Policy methods return copies:
| Method | Effect |
|---|---|
withDelay(Delay) | Uses NoDelay, FixedDelay, or ExponentialDelay. |
withFixedDelay(Duration) | Uses the same delay after each failed attempt. |
withExponentialDelay(initial, multiplier = 2u, maximum = null) | Multiplies the delay after each failed attempt, with an optional cap. |
withJitter(Jitter) | Uses the exact delay (None) or a random delay from zero to that delay (Full). |
withCondition(fn(Throwable): bool) | Replaces the rule that decides which errors to retry. |
withDeadline(Duration) | Stops after a failure if elapsed time exceeds the deadline or the next delay would pass it. |
Attempt counts must be positive. Delays and deadlines may be zero but cannot
be negative. An exponential multiplier must be at least two. An uncapped
delay that grows beyond Duration’s range throws OverflowError.
The deadline does not interrupt an operation already running. Use an operation’s
own timeout or cancellation support to bound that call. A cancellation token
passed to retry or Retrier::run cancels retry pauses; the operation must
handle its own cancellation.
Controlled time
Retrier accepts a Whim\Clock\Sleeper, a Whim\Clock\Clock, and a
Whim\RandomSequence\Sequence. It uses SystemSleeper, SystemClock, and
a securely seeded sequence by default.
A frozen clock and virtual sleeper let tests check delays without waiting:
use Whim\Clock\FrozenClock;
use Whim\Clock\VirtualSleeper;
use Whim\RandomSequence\MersenneTwisterSequence;
use Whim\Reference\Strong;
use Whim\Retry\Jitter;
use Whim\Retry\Policy;
use Whim\Retry\Retrier;
use Whim\Time\Duration;
use Whim\Unwind\RuntimeException;
$clock = FrozenClock::atUnixTimestamp(0);
$retrier = new Retrier(
new VirtualSleeper($clock),
$clock,
new MersenneTwisterSequence(7u),
);
$policy = Policy::attempts(4u)
->withExponentialDelay(Duration::second())
->withJitter(Jitter::None)
->withDeadline(Duration::fromSeconds(10));
$calls = new Strong::<int>(0);
$result = $retrier->run::<int>($policy, fn(): int {
$calls->value++;
if ($calls->value < 4) {
throw new RuntimeException('try again');
}
return 42;
});
assert!($result == 42);
assert!($clock->now()->getSeconds() == 7);
See Clocks and Sleepers, Circuit Breakers, and Rate Limits.
Circuit Breakers
Whim\Fuse\Fuse counts failures and blocks requests for a recovery period
once they reach a threshold. The caller checks allow(), runs the operation,
and reports its outcome with recordSuccess() or recordFailure().
| State | Behaviour |
|---|---|
State::Closed | Allows requests. A success clears the consecutive failure count; the failure threshold opens the fuse. |
State::Open | Blocks requests until the recovery period ends. |
State::HalfOpen | Allows requests again. The success threshold closes the fuse; any failure reopens it. |
Defaults are five failures, thirty seconds of recovery, and one success to close. Failure and success thresholds must be positive; recovery may be zero but cannot be negative.
use Whim\Clock\FrozenClock;
use Whim\Fuse\Fuse;
use Whim\Fuse\State;
use Whim\Time\Duration;
$clock = FrozenClock::atUnixTimestamp(0);
$fuse = new Fuse(
failureThreshold: 2u,
recovery: Duration::fromSeconds(30),
successThreshold: 2u,
clock: $clock,
);
assert!($fuse->allow());
$fuse->recordFailure();
$fuse->recordFailure();
assert!(!$fuse->allow());
assert!(($fuse->retryAfter() as Duration)->equals(Duration::fromSeconds(30)));
$clock->advance(Duration::fromSeconds(30));
assert!($fuse->getState() == State::HalfOpen);
$fuse->recordSuccess();
$fuse->recordSuccess();
assert!($fuse->getState() == State::Closed);
getState() and allow() check whether recovery has ended. retryAfter()
returns the remaining delay while open, or null when requests may proceed.
reset() closes the fuse and clears all counts.
A fuse uses Whim\Clock\SystemClock by default. Pass any
Whim\Clock\Clock to control time. Moving the clock backward extends the
remaining recovery period.
Share one fuse among callers that should share a failure count. Its state
lives in memory. It does not run operations, impose timeouts, or limit the
number of requests in HalfOpen. The caller controls those choices and must
report each outcome.
See Clocks and Sleepers, Retry Policies, and Rate Limits.
Rate Limits
Whim\Throttle provides two limiters. Each key has its own state and starts
with a full allowance. Keys must be nonempty strings.
| Limiter | Behaviour |
|---|---|
TokenBucket | Adds one permit per rate interval, up to a burst limit. |
FixedWindow | Grants a fixed permit count per window, starting at a key’s first hit or peek. |
Both implement Limiter:
| Method | Effect |
|---|---|
hit(key) | Takes one permit if available and returns a Decision. |
peek(key) | Returns the current decision without taking a permit. |
allow(key) | Takes a permit if available and returns a boolean. |
wait(key, cancellation = null) | Sleeps until a permit is available, then takes it. |
forget(key) | Removes the stored state for one key. |
Decision has two cases: Allowed holds the remaining permit count;
Denied holds a retryAfter duration. isAllowed() reports which case it is.
Token buckets
Rate::of(amount, per) sets a permit count per duration. perSecond,
perMinute, and perHour are shorter forms. The count and duration must be
positive. interval() divides the duration by the count, rounding down with
a minimum of one nanosecond.
The default burst limit equals the rate’s permit count. Pass a separate burst limit to change how many permits a key can store:
use Whim\Clock\FrozenClock;
use Whim\Throttle\Allowed;
use Whim\Throttle\Denied;
use Whim\Throttle\Rate;
use Whim\Throttle\TokenBucket;
use Whim\Time\Duration;
$clock = FrozenClock::atUnixTimestamp(0);
$limiter = new TokenBucket(Rate::perSecond(1u), burst: 2u, clock: $clock);
assert!(($limiter->peek('peer') as Allowed)->remaining == 2u);
assert!($limiter->allow('peer'));
assert!($limiter->allow('peer'));
$decision = $limiter->hit('peer');
$wait = match ($decision) {
Allowed => Duration::zero(),
$denied @ Denied => $denied->retryAfter,
};
assert!($wait->equals(Duration::second()));
assert!($limiter->allow('other'));
$clock->advance(Duration::second());
assert!($limiter->allow('peer'));
assert!(!$limiter->allow('peer'));
Fixed windows and waiting
FixedWindow accepts a positive limit and window duration. A new window
restores the full allowance. Requests just before and after a window boundary
can use up to twice the limit.
Both limiters use Whim\Clock\SystemClock and SystemSleeper by default.
Pass a frozen clock and virtual sleeper to test waits without real delays:
use Whim\Clock\FrozenClock;
use Whim\Clock\VirtualSleeper;
use Whim\Throttle\FixedWindow;
use Whim\Time\Duration;
$clock = FrozenClock::atUnixTimestamp(0);
$limiter = new FixedWindow(
1u,
Duration::fromSeconds(5),
$clock,
new VirtualSleeper($clock),
);
assert!($limiter->allow('worker'));
$limiter->wait('worker');
assert!($clock->now()->getSeconds() == 5);
assert!(!$limiter->allow('worker'));
The cancellation token cancels a wait’s sleep. When a permit is already
available, wait takes it without sleeping.
Limiter state lives in memory. Share an instance among callers that should
share limits, and call forget when a key no longer needs tracking. Moving
the clock backward resets a fixed window; a token bucket keeps its permit
count and starts counting elapsed time from the new time.
See Clocks and Sleepers, Retry Policies, and Circuit Breakers.
State Machines
Whim\State provides typed finite state machines. Machine<S, E> holds one
current state of type S and accepts events of type E. These may be enums,
strings, or other Whim types; the machine compares states and events with
Whim equality.
Declaring transitions
Use MachineBuilder<S, E> to set the initial state and declare allowed moves:
use Whim\State\MachineBuilder;
enum OrderState {
case Draft;
case Placed;
case Shipped;
}
enum OrderEvent {
case Place;
case Ship;
}
$builder = new MachineBuilder::<OrderState, OrderEvent>(OrderState::Draft);
$builder->allow(OrderState::Draft, OrderEvent::Place, OrderState::Placed);
$builder->allow(OrderState::Placed, OrderEvent::Ship, OrderState::Shipped);
$order = $builder->build();
assert!($order->getState() == OrderState::Draft);
assert!($order->can(OrderEvent::Place));
assert!($order->peek(OrderEvent::Place) == OrderState::Placed);
assert!($order->getState() == OrderState::Draft);
assert!($order->apply(OrderEvent::Place) == OrderState::Placed);
assert!($order->apply(OrderEvent::Ship) == OrderState::Shipped);
assert!(!$order->can(OrderEvent::Place));
$another = $builder->build();
assert!($another->getState() == OrderState::Draft);
| Method | Effect |
|---|---|
getState() | Returns the current state. |
peek(event) | Returns the first allowed target, or null when none matches. |
can(event) | Reports whether a transition accepts the event. |
apply(event) | Changes state, calls observers, and returns the target. Throws TransitionException when no transition accepts the event. |
Each build() creates a new machine with the builder’s current transition
and observer lists. Later additions to the builder do not change machines
already built. Guard and observer objects can still be shared between machines.
States may include null. In that case, peek() returning null can mean
either a valid null target or no transition. Use can() to tell them apart.
Guards and observers
Guard<S, E>::allows(from, event, to) decides whether a matching transition
may run. FunctionGuard wraps a fn(S, E, S): bool.
The machine checks transitions in declaration order. When a guard returns
false, it tries the next transition for the same state and event. The first
accepted transition wins. A transition without a guard always accepts the
matching state and event.
Observer<S, E>::onTransition(from, event, to) receives a completed move.
FunctionObserver wraps a fn(S, E, S): void. Observers run in registration
order after the machine sets its new state:
use Whim\Reference\Strong;
use Whim\State\FunctionGuard;
use Whim\State\FunctionObserver;
use Whim\State\MachineBuilder;
$ready = new Strong::<bool>(false);
$moves = new Strong::<vec<(string, string, string)>>(vec[]);
$builder = new MachineBuilder::<string, string>('queued');
$builder->allow('queued', 'start', 'running',
new FunctionGuard::<string, string>(
fn(string $_from, string $_event, string $_to): bool => $ready->value,
),
);
$builder->observe(new FunctionObserver::<string, string>(
fn(string $from, string $event, string $to): void {
$moves->value[] = ($from, $event, $to);
},
));
$machine = $builder->build();
assert!(!$machine->can('start'));
$ready->value = true;
assert!($machine->apply('start') == 'running');
assert!($moves->value == vec[('queued', 'start', 'running')]);
peek() and can() run guards but do not change state or call observers.
A guard error passes to the caller before any state change. An observer error
passes to the caller after the state change and stops the remaining observers;
it does not undo the move.
Callbacks may suspend. Coordinate callers that share a machine when callbacks can yield; the machine does not lock transitions.
You can also construct Machine<S, E> directly with an initial state, a
vec<Transition<S, E>>, and a vec<Observer<S, E>>. Each Transition holds
public from, event, to, and guard fields.
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|uint|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.
Unsigned encoding writes exact decimal digits. Integer tokens decode as int
when they fit, then uint when they fit the unsigned range; integers outside
both ranges throw DecodingException. Decimal points and exponents select
float. -0 decodes as signed zero. Oversized integer tokens do not fall back
to a rounded float.
JSON has no unsigned marker. Encoding 42u and decoding it returns signed 42.
The unsigned maximum can round-trip with its kind intact because it exceeds
the signed range. Unsigned dict keys remain invalid JSON object keys.
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.
Match offsets and lengths use uint. matches and find accept a uint
offset, defaulting to 0u; an offset beyond the subject returns no match.
replaceAll takes a null|uint limit. capture accepts a uint index or a
name, while the public captures dictionary keeps its signed numeric keys.
An out-of-range capture index 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.
Write operations return a uint byte count. Seeking accepts a signed offset
and returns a uint position. IO\copy() and IO\copy_chunked() also return
the number of bytes copied as uint.
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. Its memory threshold takesuint.
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\read takes a uint byte offset, and truncate takes a uint length.
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.
Metadata byte sizes, block counts, and link counts use uint, as do the
available and total disk-space results.
Callers must pass true to delete a directory tree. Permission values use
unsigned octal literals such as 0o755u.
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 / on Unix and \ on Windows. Windows also accepts
/ in ordinary paths. Windows paths use UTF-8, with WTF-8 for unpaired UTF-16
surrogates, so names read from the filesystem can be passed back unchanged.
File descriptors
OS\FileDescriptor owns a Unix descriptor or a Windows handle or socket.
duplicate($number) creates a
new owned descriptor from an open number. toUint() returns its number as uint.
isClosed() and close() manage its lifetime.
Duplicating is not the same as borrowing an integer. The new object owns its descriptor and closes it.
Windows platform limits
Files, pipes, TCP, UDP, TLS, Unix stream sockets, child processes, timers, and
coroutines use Windows APIs. Unsupported POSIX operations throw
Whim\Unwind\UnsupportedPlatformException before starting the operation:
- Unix user and group IDs, account lookups, and supplementary groups.
- Process sessions and groups, POSIX priorities, and resource limits.
- Replacing the current process with
Process\replace. - File ownership, POSIX permission changes, creation masks, and filesystem FIFOs.
- Unix datagram sockets and
SO_REUSEPORT. - Sending UDP packets marked
ExplicitCongestion::CongestionExperienced. - POSIX load averages and signals.
Signal::isSupported()returnsfalse.
On Windows, Child::kill() and Child::terminate() end the child immediately.
They return Command\Exited with the Windows exit code. Windows exit codes
use the full unsigned 32-bit range. terminate() cannot first send SIGTERM.
File metadata reports -1 for Unix user and group IDs. Its permission bits
describe the read-only attribute rather than a POSIX access mode. New files
inherit Windows access-control rules from their parent directory.
Creating symbolic links requires Developer Mode or the Windows symlink privilege.
The package manager stores Git executable bits in named file streams so package checksums agree across platforms. Use NTFS or another filesystem that supports these streams for the project and package cache.
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.
Process, user, group, and session IDs use uint, as do resource limits.
Command\Child::getId() returns the same unsigned process ID accepted by the
process functions.
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 as uint values.
Users and groups
OS\find_user() and OS\find_group() query the operating-system account
directory by name or a uint 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 positive uint 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 a Uint16 port.
Ports use uint literals such as 443u; 0u asks the system to choose a local
port when binding. 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. Received
and outgoing interface indices use uint.
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.
On FreeBSD, IPv4 send metadata supports source addresses and congestion bits,
but not an explicit outgoing interface. Leave interfaceIndex in SendMetadata
at 0u for IPv4; a nonzero value throws NetworkException. Received datagrams
still report their interface index, and IPv6 supports selecting an interface.
A socket bound to a specific IPv4 address must use that source address.
On FreeBSD 14.4, sockets bound or connected to an IPv4-mapped IPv6 address do not
receive packet metadata: datagrams report an interface index of 0u and
ExplicitCongestion::NotCapable. Bind to an IPv4 address, or use an unconnected
dual-stack socket bound to ::, when you need that metadata.
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.count()returns the number of entries asuint.toVec,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.
Record revisions and the revision passed to Store::replace use uint,
starting at 0u.
Store::rotate takes the old identifier, expected revision, new identifier,
data, and TTL. It replaces the old record with the new one atomically and
starts the new record at revision 0u. A missing, expired, or changed source,
or a live record at the new identifier, causes ConflictException. A conflict
leaves live records unchanged. The session middleware uses this operation when
a loaded session requests a new identifier.
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.
The informational-response limit uses uint, such as 16u. Pool limits and
the redirect limit also use uint; pass 0u when disabling an idle pool.
Response header and body size limits use NonZero<uint>, such as 8192u.
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.
Origin and endpoint ports use uint in the range 0u..=65535u.
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() returns their number as uint.
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. The backoff multiplier uses NonZero<uint> and defaults to 2u.
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.
Frame, message, header, and read sizes use NonZero<uint>.
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
For event streams, use Whim\HTTP\SSE. It encodes events and
provides a response body that the server can stream as events arrive.
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 and closes idle HTTP/1 connections without sending a
timeout response. Active requests can finish until shutdownTimeout expires;
the server then cancels their work and closes the 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.
The request-body size and server-push limits use uint, such as
maximumRequestBodySize: 10_485_760u and maximumServerPushes: 100u.
Connection, concurrent-request, header-size, and requests-per-connection limits
use NonZero<uint>, such as maximumConnections: 1_000u.
CORS::maxAge and RequestDecompression::maximumDecodedBytes also use uint
values; maxAge may be null.
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.
Server-Sent Events
Whim\HTTP\SSE encodes server-sent events and supplies streaming HTTP response
bodies. It ships with stdlib and needs no package or autoloader.
Events
An Event carries UTF-8 text, an optional name, an optional ID, and an optional
reconnect delay:
use Whim\HTTP\SSE\Event;
use Whim\Time\Duration;
$event = new Event(
'{"temperature": 21}',
name: 'reading',
id: '42',
retry: Duration::fromSeconds(3),
);
assert!(
$event->encode()
== "id: 42\nevent: reading\nretry: 3000\ndata: "
. '{"temperature": 21}'
. "\n\n",
);
encode() writes a data: field for each payload line and ends the event with
a blank line. It converts CR and CRLF line endings to LF. Empty data still
writes a field, so the browser delivers the event.
Null omits a name or ID. An empty name selects the default message event;
an empty ID clears the browser’s resume position. Names and IDs cannot contain
line breaks, and IDs cannot contain NUL. All text must be valid UTF-8. Invalid
input throws Whim\Unwind\InvalidArgumentException.
Retry durations must be non-negative. Encoding drops fractions of a
millisecond and handles Duration::max() without integer overflow.
Comment::of($text) encodes a comment and replaces line breaks with spaces.
Browsers ignore comments; a server can use them as heartbeats.
Sources and response bodies
Implement Whim\HTTP\SSE\Source\Source to supply events:
use Whim\Async\CancellationToken;
use Whim\HTTP\SSE;
use Whim\HTTP\SSE\Event;
use Whim\HTTP\SSE\Source\Source;
final class Greetings implements Source {
private bool $finished = false;
public function poll(null|CancellationToken $cancellation = null): vec<Event> {
$cancellation?->throwIfCancellationRequested();
$this->finished = true;
return vec[new Event('hello', name: 'greeting', id: '1')];
}
public function isFinished(): bool {
return $this->finished;
}
}
$response = SSE\respond(new Greetings());
assert!($response->status == 200);
assert!($response->body?->readAll() == "id: 1\nevent: greeting\ndata: hello\n\n");
respond($source, $headers = FieldMap::default()) returns status 200 with
Content-Type: text/event-stream; charset=utf-8, Cache-Control: no-store,
and X-Accel-Buffering: no. It keeps other caller headers and removes
Content-Length and Transfer-Encoding, letting the server choose framing.
The HTTP/1.1 server streams chunks; the HTTP/2 server streams DATA frames.
Body implements Whim\IO\ReadHandle and Whim\IO\CloseHandle. A read polls
only after it drains buffered events. An empty batch produces Comment::WAITING
instead of ending the stream. The body ends after the source finishes and its
buffer drains. An already finished source receives no polls.
For a long-lived source, poll() must wait for events or a heartbeat interval
and honor its cancellation token. Returning empty batches at once in a loop
would consume CPU. Source\FunctionSource wraps a polling callable with the
same signature; it never reports itself finished.
The body checks cancellation before and after each poll. Source errors reach
the reader unchanged. Closing the body discards buffered bytes and closes a
source that implements CloseHandle. A suspended poll cannot refill a body
after another task closes it. Use using when reading a body directly. If a
handler keeps a body or source, register Body::close() through
Context::onResponseComplete() to release it when the response ends.
Reconnection and completion
SSE\last_event_id($request) reads Last-Event-ID, returning null when the
field is absent or empty. The application decides how to resume from that ID;
the library stores no event history.
SSE\refuse($event, $headers = FieldMap::default()) sends one event with status
200 and ends the response. This alone does not stop reconnection. The client
must call EventSource.close() after an application-defined terminal event:
const events = new EventSource('/events');
events.addEventListener('done', () => events.close());
events.addEventListener('failed', () => events.close());
A successful stream ending normally makes EventSource reconnect. To tell a
browser to stop without sending an event, return an HTTP 204 response. These
rules follow the SSE specification.
See HTTP Server for handlers and response completion, and HTTP Messages and Cookies for response values.
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.
getAffectedRows() returns null|uint.
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 as uints. 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. The timeout in milliseconds and statement-cache capacity take uints.
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.
MIME\MultiPart\Parser takes uint values for its spool threshold, maximum
part count, maximum part size, and maximum header size. 0u means no limit
for each maximum.
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.
SMTP\Client\Connection::maximumMessageSize() returns the advertised size
limit as null|uint.
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.
maximumIdleConnections uses uint and defaults to 1u; 0u disables idle
connection reuse.
The chunk size uses NonZero<uint> and defaults to 65_536u.
Ports use uint in the range 0u..=65535u. When the caller omits a port,
STARTTLS uses 587u, implicit TLS uses 465u, and plaintext uses 25u.
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.
Both take a uint length, such as 32u.
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 unsigned 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 | define attribute classes and targets |
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\Fn | identity, observation, composition, branches, and rethrowing |
Whim\GC | explicit cycle collection |
Whim\Lint | scoped lint settings |
Whim\Logic | single-assignment variables, linking, and waiting |
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 | files, declarations, types, values, and invocation |
Whim\Reflection\Attribute | attribute rules and target kinds |
Whim\Reflection\Generic | type parameters, bindings, and type environments |
Whim\Reflection\Member | methods, properties, constants, and enum cases |
Whim\Reflection\Type | type forms and their parts |
Whim\Result | Ok, Err, and throwable capture |
Whim\State | typed state machines, guards, and observers |
Whim\Symbol | symbol lookup and symbol kinds |
Whim\Type | engine-local type identifiers |
Whim\Unwind | errors, exceptions, throwables, and trace frames |
Whim\Variation | values with correlated choices |
See Core Types and Functions, Function Helpers, Reflection, Option and Result, and Built-in Attributes.
See State Machines for typed transitions, guards, and observers.
See Variation and Logic Variables for values with explicit alternatives or later bindings.
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\UInt | unsigned integer parsing and exact division |
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\Graph | directed and undirected graphs, traversal, and paths |
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\Tree | typed trees, traversal, search, mapping, and folds |
Whim\UUID | UUID parsing plus versions 4 and 7 |
See Graphs, Trees, and Encoding and Data Formats.
Time, environment, and the operating system
| Namespace | Purpose |
|---|---|
Whim\Clock | system and controlled clocks and sleepers |
Whim\Command | child-process setup and control |
Whim\Cron | cron parsing, matching, and next or previous occurrences |
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, Clocks and Sleepers, Cron Expressions, 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.
Retries and request limits
| Namespace | Purpose |
|---|---|
Whim\Fuse | circuit breakers with failure and recovery thresholds |
Whim\Retry | retry policies, delays, jitter, and deadlines |
Whim\Throttle | keyed token buckets and fixed windows |
See Retry Policies, Circuit Breakers, and Rate Limits. All three use Clocks and Sleepers.
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\SSE | server-sent events and streaming response bodies |
Whim\HTTP\SSE\Source | event sources and polling function adapters |
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, HTTP Server, and Server-Sent Events.
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
fresh 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 uint interface mixed namespace never
newtype object out private protected
public readonly string type use
vec void where
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 |
| Conditional | ? : | 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 |
cpu_architecture!() | string | returns the platform’s CPU architecture |
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 |
executable_extension!() | string | returns the executable extension without a dot |
executable_suffix!() | string | returns the executable suffix, including a dot if present |
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 |
fresh!() | fresh | creates a distinct identity within the VM lifetime |
length!($value) | uint | counts string bytes or array items |
operating_system!() | string | returns the platform’s operating system |
operating_system_family!() | string | returns the platform’s operating system family |
panic!($message) | never | requires a string, 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 |
sequence!($first, ..., $last) | last expression’s type | evaluates one or more expressions in order and produces the last value |
shared_library_extension!() | string | returns the shared library extension without a dot |
shared_library_prefix!() | string | returns the shared library filename prefix |
shared_library_suffix!() | string | returns the shared library suffix, including the dot |
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! evaluates one expression that must produce a string, then prints the
message and current stack trace to standard error and exits with status 255.
A non-string result throws a catchable TypeError instead. Errors during
message evaluation propagate normally. Trace boundaries and sensitive
parameter markers apply. Both exit 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.
Both are valid in constant expressions.
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)* ","? "]"
file-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 := file-attribute-list
| 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. Declarations, imports, and file attributes belong at the top level of a file or namespace.
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? where-clause? 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 uses fn and captures outer variables by value. Its body is one
expression or a block.
closure := attributes? "fn" type-parameters? parameter-list
return-type? where-clause? closure-body
closure-body := "=>" expression | block
Visibility and readonly on a parameter promote it to a property and are
valid only in a class constructor.
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? where-clause? (block | ";")
where-clause := "where" where-constraint ("," where-constraint)* ","?
where-constraint
:= identifier ":" type
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.
A callable’s where clause requires
each named type parameter to fit its bound.
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 := 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
| match-expression
| "new" class-expression call-arguments?
| "break" integer-literal?
| "continue" integer-literal?
| "return" expression?
| "throw" expression
| unary-expression
| binary-expression
| conditional-expression
| assignment-expression
| call-expression
| partial-call
| member-expression
| index-expression
| construct-expression
conditional-expression
:= expression "?" expression ":" 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.
The sequence! construct accepts one or more expressions, with an optional
trailing comma:
sequence-expression
:= "sequence" "!" "(" expression ("," expression)* ","? ")"
fresh-expression := "fresh" "!" "(" ")"
Both are construct expressions. See Language Constructs for evaluation and discard rules.
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 Callables and Partial Function Application.
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 := intersection-pattern ("@" union-pattern)?
intersection-pattern
:= primary-pattern ("&" primary-pattern)*
primary-pattern := variable
| prefix-type
| "(" pattern ")"
| tuple-pattern
| vec-pattern
| dict-pattern
| object-pattern
| named-object-pattern
named-object-pattern
:= qualified-name ("<" type-list ">")? object-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 | "true" | "false")
"=>" pattern
object-pattern := "#{" object-pattern-items? "}"
object-pattern-items
:= object-pattern-entry ("," object-pattern-entry)*
("," "...")? ","? | "..." ","?
object-pattern-entry
:= identifier ":" pattern | variable
trailing-pattern
:= "..." pattern?
Variables bind. Types and literals check. @ requires both patterns to match
the same value, as does &. Tuple, vec, dict, and object patterns may nest.
Tuple, vec, and dict patterns may end with ... to accept, check, or bind the
rest. Object patterns use a bare ... to permit additional public properties.
A pattern after a collection’s ... cannot contain bindings, except for a
whole-remainder capture written as $rest or $rest @ pattern, where the
pattern on the right contains no bindings. The compiler enforces this rule
recursively; parentheses preserve the same meaning.
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 := object-shape-type
| named-shape-type
| built-in-type
| named-type
| literal-type
| range-type
| string-length-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"
named-shape-type := qualified-name ("<" type-list ">")? object-shape-type
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" | "uint" | "float" | "string"
| "fresh" | "object" | "mixed" | "never" | "void"
literal-type := literal | "-" (integer-literal | float-literal)
range-type := signed-integer-literal (".." | "..=")
signed-integer-literal?
| (".." | "..=") signed-integer-literal
string-length-type
:= "string" "[" (integer-literal | string-length-range) "]"
string-length-range
:= integer-literal (".." | "..=") integer-literal?
| (".." | "..=") 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 | "true" | "false") "=>" type
dict-shape-rest := "..." ("<" type "," type ">")?
object-shape-type
:= "#{" object-shape-items? "}"
object-shape-items
:= object-shape-entry ("," object-shape-entry)*
("," "...")? ","? | "..." ","?
object-shape-entry
:= identifier ":" 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 fn value created by a closure expression, first-class callable syntax, or
partial function application.
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 written with fn. Its body is an expression or block, and
it captures the outer variables it uses by value.
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, uint, string, or fresh keys.
First-class callable
A function or method saved as a fn value with foo(...) or
$object->method(...).
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 function application
A call expression that creates a callable with selected arguments left open,
such as foo(?, 'fixed').
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 uint 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
Why does Whim exist?
Whim is an experimental programming language inspired by PHP and Hack. As a research project, it explores language ideas by putting them into practice, with the hope that useful ones can contribute to PHP’s future.
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.
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 an experimental research project. It makes no production support or security guarantees.