Skip to Content
Cloth

Nullability

Cloth treats every type as non-nullable by default. A binding declared Speaker s always holds a real Speaker — assigning the literal null to it is a compile-time error. To opt a binding into accepting null, append a ? to its type: Speaker? s.

The compiler enforces these rules at every assignment, return, and parameter pass. The ? suffix is part of the type, so Speaker and Speaker? are distinct types: a nullable value cannot silently flow into a non-nullable target.

Declaring a nullable

A type expression becomes nullable when followed by ?. The ? attaches to the full type, after any array brackets:

Speaker? s; // a nullable class reference i32? n; // a nullable integer (parses; see Limitations below) string[]? xs; // a nullable array of strings

A nullable binding may be initialized with the bare literal null, with a real value of the underlying type, or left for an explicit assignment later:

Speaker? optS = null; Speaker? present = new Speaker(); // T → T? lifting is always allowed

The null literal

null is a value, not a type. It is assignable only to a nullable target:

Speaker s = null; // S006: expected 'Speaker', got 'null' Speaker? s = null; // ok i32 n = null; // S006

T → T? lifting

A non-nullable value is always assignable to a nullable slot. Lifting never fails because adding the possibility of null cannot violate any contract.

Speaker speaker = new Speaker(); Speaker? optS = speaker; // ok

T? → T is rejected

The reverse is not legal. A nullable value cannot flow into a non-nullable target — that would lose the safety guarantee:

Speaker? optS = ...; Speaker s = optS; // S006: expected 'Speaker', got 'Speaker?'

let is always non-nullable

A let binding infers its type from the right-hand side. Because let is non-nullable by definition, the initializer must produce a non-null value. The bare null literal and any nullable expression are both rejected:

let x = null; // S006: `let x` cannot bind a nullable value (got 'null') Speaker? optS = ...; let y = optS; // S006: `let y` cannot bind a nullable value (got 'Speaker?')

If you need to keep nullability, use a typed declaration:

Speaker? y = optS; // ok

If you need a non-null value extracted from a nullable, use the null-coalescing operator to provide a fallback (see below).

Member access, method calls, and indexing

A nullable value cannot be dereferenced. Field access, method calls, and indexing on a nullable target are all rejected at compile time:

Speaker? optS = ...; optS.greet(); // S020: cannot call 'greet' on nullable 'Speaker?' optS.name; // S020: cannot access 'name' on nullable 'Speaker?' maybeArr[0]; // S020: cannot index nullable 'string[]?'

To use the value, you must first establish it is non-null. Today that means coalescing it with a fallback (see below); future cast forms will add additional patterns.

The ?? null-coalescing operator

x ?? y evaluates to x when x is non-null and to y otherwise. The result type is the non-nullable form of x’s type, so a coalesced expression can be assigned to a non-nullable target or bound with let:

Speaker? optS = ...; Speaker s = optS ?? new Speaker(); // s is Speaker (non-null) let chosen = optS ?? new Speaker(); // chosen is Speaker

Comparing to null

x == null and x != null are valid for any type. They produce a bool:

Speaker? optS = ...; if (optS == null) { println("optS is null"); }

The compiler does not narrow the type inside an if (x != null) branch — x still has type T? in the body. Extract a non-null value with ?? (or, in the future, with the cast forms once they land) when you need to use it as T.

Implementation status

The rules above are enforced wherever the analyzer assigns or compares types, and ?? lowers to LLVM end-to-end for class- and interface-typed nullables. A few pieces are still pending:

  • Primitive nullable types (i32?, f64?, bool?) parse and the type system treats them as nullable, but they have no runtime tag. Storing the literal null into a primitive nullable produces invalid LLVM IR. Stick to class- and interface-typed nullables for now; primitive nullability will land alongside a tagged storage layout. Using ?? on a primitive nullable also fails (L004 at codegen) because the underlying check is a pointer-null comparison.
  • Flow typing is not implemented. The body of if (x != null) does not narrow x from T? to T. Use ?? to obtain a non-null value.
  • Definite-assignment analysis is not implemented. A non-nullable class field declared without an inline initializer is zero-initialized to null at allocation time, and the compiler does not yet verify that every constructor sets it. Initialize non-null class fields in every constructor or declare them T?.

Diagnostics

CodeFires when
S006 TypeMismatchnull assigned to a non-nullable target; T? flowing into a T; let binding a null literal or a nullable value.
S020 NullableDerefField access, method call, or indexing on a T? value.