Nullability
Cloth types are non-null by default. Add one ? to admit absence:
string? name = null;
int32? count = 3;
Status? status = Status.Ready;
Point? point = Point(1, 2);Primitive, enum, struct, class, interface, string, object, and array values can
be nullable. void, null, and an already nullable type cannot be followed by
?. Overloads cannot differ only by nullability.
Nullable references retain their reference representation. Nullable primitive, enum, and struct values are inline tagged values; they are not boxed objects. This representation is compiler-owned and is not a persistence format.
Assignment, conversion, and inference
T widens to T?, and null converts to any T?. A nullable value cannot be
used as T without a proof, fallback, or assertion. Numeric widening lifts
through nullability:
int16? small = 12;
int32? wide = small;
var inferred = true ? 12 : null; // int32?The lifted conversion preserves absence and converts only a present payload.
Compatible class, interface, and object widening also composes with
nullability. Narrowing conversions still require an explicit cast.
Array nullability is independent of element nullability: User?[], User[]?,
and User?[]? mean different things. Nullable value elements such as int32?[]
and Point?[] are supported.
Mutable nullable locals and mutable nullable fields default to null when no
initializer is present. Final locals still require an initializer. Constructors
must initialize every struct field, including nullable fields.
Narrowing locals and parameters
func Display(string? value): string {
if (value == null) {
return "unknown";
}
return value;
}A direct null comparison proves presence on the appropriate path. Reversed operands, parentheses, negation, and short-circuit logical expressions compose these proofs.
The binding retains its declared nullable type. Reads on a proven path use the non-null type; assignment invalidates that proof. Fields are not narrowed because an alias or function call could change them. Copy a field into a local before checking it when stable narrowing is required.
Presence conditions
Any nullable value can be used as a condition:
bool? enabled = false;
if (enabled) {
println("present, even though the payload is false");
}The condition tests presence, not the payload. This rule is especially important
for bool?: both true and false are present. !value, &&, and || use the
same presence rule. A non-null value condition is rejected unless its ordinary
type is bool.
Safe fields and calls
receiver?.Field evaluates the receiver once. If absent, it returns null;
otherwise, it reads the field. A T result becomes T?, while a T? result
stays nullable.
User? user = FindUser();
string? name = user?.Name;
int32? age = user?.Age;
Point? position = user?.Position;The same operator safely invokes declared instance functions:
int32? age = user?.GetAge();
Point? moved = point?.Moved(1, 2);
logger?.Flush();On absence, arguments are not evaluated and a throwing call cannot throw.
A safe void call performs no action and produces void. Static functions,
constructors, and unresolved members cannot be called safely.
Safe indexing and slicing are not supported. Narrow or assert the receiver before those operations.
Safe meta queries
Use ?:: for a non-callable meta query:
int32? length = text?::length;
string? kind = value?::typeName;The receiver is evaluated once. Absence yields null; presence performs the same
query as ::. Callable meta operations such as parse, slice, wrap, and
sat do not support ?::.
Fallback and assertion
string? selected = null;
string display = selected ?? "Unknown";
int32 count = maybeCount!;?? evaluates its left operand once and evaluates the fallback only when it is
absent. A non-null compatible fallback yields a non-null result; a nullable
fallback keeps the result nullable. The operator associates to the right.
Postfix value! asserts presence and yields the non-null payload. It terminates
with non-null assertion failed if absent. A successful assertion also narrows
subsequent reads of a stable local or parameter.
Two nullable values of the same underlying type support == and !=. Two
absent values are equal; one absent and one present value are unequal; two
present values use the underlying type’s equality rule.