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.