Skip to main content
Next (unreleased) · docs c218d875 · core f63c1e69 · built 2026-10-26 22:39 UTC

Type System

Metel is statically and strongly typed. Types are checked at compile time. There are no implicit conversions.

Primitive Types​

TypeDescriptionExample
i6464-bit signed integer42
f6464-bit floating point3.14
booleanBooleantrue
StringUTF-8 string"hello"
CharUnicode scalar value'a'
()Unit — represents no value()

The unit type () is only written explicitly when needed as a type parameter (e.g. Result<(), Error>). Functions that return nothing omit the -> annotation entirely.

Sized Numeric Types​

Sincev0.8.0

Metel provides exact-width numeric types for low-level and systems programming. i64 and f64 are the default integer and floating-point types in ordinary code.

Signed integers:

TypeWidth
i88-bit
i1616-bit
i3232-bit
i6464-bit

Unsigned integers:

TypeWidth
u88-bit
u1616-bit
u3232-bit
u6464-bit

Floats:

TypeWidth
f3232-bit IEEE 754
f6464-bit IEEE 754

Sized literals use a suffix: 42i32, 3.14f32, 255u8. All casts between sized numeric types are explicit (as). Array indices must be u64; indexing with an i64 requires an explicit as u64 cast.

Unsuffixed literals are polymorphic. When the expected type is known from context (annotation, function parameter, struct field, return type, or the other operand in arithmetic/comparison), an unsuffixed numeric literal adopts that type automaticallyL3. When no context is available, the literal defaults to i64 (integer) or f64 (float).

let a: i32 := 10; // 10 is i32
let b: u8 := 255; // 255 is u8
let c: f32 := 1.5; // 1.5 is f32

fun scale(x: f32, factor: f32) -> f32 { x * factor }
let r := scale(2.0, 3.0); // both literals are f32

let x: i32 := 10i32;
let y := x + 5; // 5 adopts i32 from x; y is i32

This also applies to var reassignment — the right-hand side of m := expr adopts m's declared type:

var count: i32 := 0;
count := 99; // 99 is i32
Formal rules
Legality Rule №1​

The exact-width numeric primitive types are i8, i16, i32, i64, u8, u16, u32, u64, f32, and f64.

Referenced by: rfc-0007

Tested by (2)
Legality Rule №2​

Conversion between numeric types is written with an explicit as cast.

Referenced by: rfc-0007

Tested by (2)
Legality Rule №3​

An unsuffixed numeric literal adopts the numeric type supplied by context; without context, integer literals default to i64 and floating-point literals to f64.

Referenced by: rfc-0007

Dynamic Semantics №1​

Integer overflow panics, unconditionally — Metel has no debug/release build-mode distinction of its own (the interpreter takes no such flag), so this applies the same way regardless of how the interpreter binary happens to have been compiled. Floating-point overflow follows IEEE 754 behavior.

Referenced by: rfc-0007

Char​

Sincev0.8.0

Char represents a single Unicode scalar value. Character literals use single quotes: 'a', '\n', '\u{1F600}'.

fun main() {
let c: Char := 'a';
let code: u32 := u32::from(c);
let back: Char := Char::from(code);
}

Char is not u32 and not a string — no implicit coercions exist. Use u32::from(c)Char methods D1 to get the Unicode scalar value and Char::from(n)Char methods D1 to construct from a code point; Char::from raises a runtime error if n is not a valid Unicode scalar value.

Formal rules
Legality Rule №1​

Char is a distinct Unicode-scalar type, not an alias for u32 or u8.

Referenced by: rfc-0007

Type Inference​

GapGAP-TYPES-001A function parameter is a monotype, so it cannot be used at two types in one body.

Types are inferred using the Hindley-Milner algorithm with let-polymorphism. Annotations are optional for all bindings, including function parameters and return types. They may be written explicitly for documentation or to restrict a binding to a less general type.

Annotations are required only where there is no expression to infer from:

  • Struct and enum field types
  • Aspect method signatures

Every named type in an annotation must resolve in the annotation's declaring scope, including names nested inside arrays, tuples, function types, and record fields. This is checked when the declaration is type-checked, even if no value ever reaches the annotation. A generic parameter in scope and Self where it is permitted resolve as types; every other unknown name is error T0003.

fun add_annotated(a: i64, b: i64) -> i64 { a + b }
fun add_inferred(a, b) { a + b }

fun main() -> i64 {
let x := 42; // inferred: i64
let name := "Vlad"; // inferred: String
let y: f64 := 3.14; // explicit annotation (optional here)
let total := add_annotated(x, 1) + add_inferred(2, 3);
if (name == "Vlad") { total + (y as i64) } else { 0 }
}
Formal rules
Legality Rule №1​

An expression in return position is typechecked against the enclosing function or method's declared return type, which supplies its expected type.

Referenced by: rfc-0019

Legality Rule №2​

An expression in break position is typechecked against its enclosing loop's value type, independently of the enclosing function's return type.

Referenced by: rfc-0019

Tuples​

Tuples are lightweight anonymous product types.

GapGAP-TYPES-009RFC-0178RFC-0151A tuple cannot be split into its first element and the rest, or extended, in a generic body.
fun main() -> i64 {
let coord: (i64, i64) := (10, 20);
let triple: (String, i64, boolean) := ("yes", 42, true);
return coord.0 + triple.1;
}

Positional field access uses zero-based selectors .0, .1, etc.L1:

fun main() -> i64 {
let coord: (i64, i64) := (10, 20);
let x := coord.0;
let y := coord.1;
return x + y;
}

() is the zero-element tuple (unit type).

Tuples can be destructured in match:

fun main() -> i64 {
let coord: (i64, i64) := (10, 0);
match (coord) {
(0, y) => y,
(x, 0) => x,
(x, y) => x + y,
}
}
Formal rules
Legality Rule №1​

A tuple's elements are addressed by zero-based positional selectors. A selector is valid only for an element in the tuple's declared arity.

Tested by (2)

Records and Rows at a Glance​

Sincev0.14.0This section is a map; the rules are stated where each topic is specified, linked below

Every product type in Metel has a row: the set of its labelled fields, each with a type. An §Anonymous records is its row. A struct or record declaration is a brand (its name, fixed at declaration) paired with a row. Rows are what generic code can talk about structurally, in three increasingly general ways:

To sayWriteSpecified in
"a record with at least these fields"<record T: { x: f64, .. }>§Row bounds
"a record with no such field"<record T: !{ token }>§Row bounds
"…and name the rest, so a signature can carry it"<row R> with { x: f64, ..R }§Open rows
"…the rest is this field plus a remainder"where R = { token: Token, ..Rest }§Open rows
"every field's type satisfies an aspect"where all R: Copy§Field wise row constraints
"this impl applies to every record of this shape"extend<row R: { x, .. }> { ..R }: AImplementing an aspect for a record§Implementing an aspect for a…

Which types a row bound or row-conditional impl can match is decided by the declaration kind, not by the fields written:

row visible to bounds and row-conditional implsinherent methods
anonymous recordyesno
record (§Records)yesyes
structnoyes
enumnoyes

Narrowing connects these to ownership. Moving a field out of a value leaves a value with a smaller row (§Narrowing); a row-conditional impl or bound is checked against that current row. Passing a wider record by value to a parameter that names only some of its fields is width subtyping, and is allowed only when no non-Copy field would be forgotten (§Open rows).

The example below uses each piece once:

record Session<row R> { id: i64, data: { ..R } }

// A bound: any record carrying `user: i64`.
fun user_of<record T: { user: i64, .. }>(e: T) -> i64 { e.user }

// A row variable: carry the rest through unchanged.
fun keep<row R>(p: { user: i64, ..R }) -> { user: i64, ..R } { p }

// Row-conditional methods: which exist depends on the session's row.
extend<row R: { token: String, .. }> Session<..R> {
fun send(self) -> i64 { self.id * 10 }
}
extend<row R: !{ token }> Session<..R> {
fun login(self) -> i64 { self.id }
}

fun main() {
assert(user_of({ user = 7, kind = 1 }) == 7);
assert(keep({ user = 1, extra = 2 }).extra == 2);

let anon: Session<{ user: i64 }> := Session { id = 2, data = { user = 1 } };
let authed: Session<{ token: String }> := Session { id = 1, data = { token = "t" } };
assert(anon.login() == 2);
assert(authed.send() == 10);
}

Anonymous Records​

Sincev0.12.0

A record is a product type whose components are labelled, where a tuple's are positional. It is written in bare braces, with no keyword:

{ x: f64, y: f64 } // the type
{ x = 1.0, y = 2.0 } // a value of it

Field declarations classify and take :; field initializers define and take = — the same distinction let x: i64 = 1 already draws.

A record type is exact. { x: f64 } is inhabited only by records with that row and nothing else; a value of { x: f64, y: f64 } is not a value of { x: f64 }. Records are not implicitly widened or narrowed.

Records are structurally typed. Two records with the same labels and field types are the same type, wherever they were written. A record has no declaration site and no name. Field order does not matter: { x: i64, y: i64 } and { y: i64, x: i64 } are the same type, and { x = 1, y = 2 } and { y = 2, x = 1 } are indistinguishable — each is usable wherever the other is. A record is a set of labelled fields, not an ordered one. Repeating a label in one record ({ x: i64, x: f64 }) is an error.

(Indistinguishable is a statement about the type, not about ==, which no compound type — record, struct, tuple, or array — supports.)

When a local variable has the same name as a field, the = value part may be omitted, as in a struct literal:

fun main() {
let x := 1.0;
let y := 2.0;
let p := { x, y }; // { x: f64, y: f64 }
println("${p.x}");
}

Punning, and single-field record literals generally, are read as records only in positions that expect an expression — a let/var or field initializer, a call argument, an array element. In a position that also admits a block — an if/else or match arm, a function, closure, or loop body — a bare { x } is a block whose result is x, and { x = 1 } is a block whose result is the assignment. Write the record in parentheses to force it: ({ x }). A multi-field literal needs no parentheses, as { x = 1, y = 2 } cannot be a block.

Where records may be used​

Records are ordinary values: they may appear as parameters, returns, let bindings, and struct or enum fields; they may be pattern-matched, used as generic arguments, and borrowed (&{ x: f64 }) exactly as a struct is. Send and Sync extend to them by the same field-composition rule used for structs.

Three things a record cannot do, all for the same underlying reason — it has no nominal owner:

  • No inherent methods. Two unrelated modules could otherwise write conflicting methods for the same shape with no principled way to choose between them.
  • No implementations of a non-local aspect, by the other direction of that rule. An aspect local to the current module may be implemented for a record: for one concrete row, or for every row of a given shape (Implementing an aspect for a record§Implementing an aspect for a…).
  • No custom Drop. Drop is a standard-library aspect and never local to ordinary user code, so teardown logic belongs to nominal types only.

Arrays are the other structural target: extend<T> [T]: MyAspect { … } is supported, per the orphan-rule carve-out for structural type constructors — see §Structural aspect bounds. Auto-derived aspects are unaffected.

Projection​

A nominal type's row may be projected to a named subset, written with a dot to distinguish it from a struct literal:

Handle.{ fd } // the type: Handle's row, narrowed to `fd`
Handle.{} // the type: Handle's brand with no fields remaining

A bare identifier inside projection braces is always a field label, never a type or a row variable. Chained projection (S.{ a }.{ b }) and projection in pattern position are not accepted.

Sincev0.14.0An empty projection in type position names the empty branded residual

If the nominal type itself declares no fields, this is a full-width projection and normalizes to that nominal type. An empty branded residual of a non-empty type is not the anonymous record type {} or Unit.

Inside an extend block, Self.{ fd } projects Self's own row exactly as Handle.{ fd } would project Handle's — Self resolves to the enclosing block's target type here the same way it does everywhere else the target's name can stand in for it.

Formal rules
Dynamic Semantics №1​

Record identity is structural: records with the same labelled fields and field types are the same type regardless of declaration-free spelling order.

Referenced by: rfc-0116

Legality Rule №1​

An anonymous record satisfies an impl-based aspect bound only through an extend on a record target whose row it has (Implementing an aspect for a record§Implementing an aspect for a…); with no such implementation the bound is rejected with T0012.

Referenced by: rfc-0116

Dynamic Semantics №2​

Projection Handle.{ fd, mode } yields the record made from precisely the named fields of the nominal receiver type.

Referenced by: rfc-0116

Tested by (3)
Legality Rule №3​

An anonymous record is rejected as an inherent-implementation target, as the target of a non-local aspect implementation, and as the target of a custom Drop implementation.

Referenced by: rfc-0116

Tested by (3)

Arrays​

Array<T> is the built-in ordered sequence type. The shorthand [T] is preferred.

Changed inv0.14.0RFC-0171the shorthand is now prefix [T] rather than postfix T[]
fun main() -> i64 {
let nums: [i64] := [1, 2, 3];
let names: Array<String> := ["alice", "bob"];
if (names.len() == 2) { nums[0] } else { 0 }
}

Index access uses [] with a u64 index. Out-of-bounds access causes a panic.

fun main() -> i64 {
let nums: [i64] := [1, 2, 3];
let first := nums[0];
return first;
}

Arrays are usable in for-in loops.

Changed inv0.12.0RFC-0126[T] is no longer an owning, mutable buffer

[T] is a non-owning, immutable, unconditionally-Copy view over a contiguous run — a pointer and a length — produced only by borrowing a List<T>, a [T; N], or another slice. Assignment through a slice, such as a[0] = 9, does not compile; mutation belongs to List<T> or [T; N]. Array literals produce [T; N] (below), not [T]; let nums: [i64] = [1, 2, 3]; continues to work through [T; N]'s implicit coercion to [T] (RFC-0053), not because the literal itself is a [T].

The three-way split between [T], [T; N], and List<T> below reflects the current design.

GapGAP-TYPES-002How a growable List<T> allocates and grows its storage, and where it ends and T[] and [T; N] begin, is not fully specified.
Formal rules
Legality Rule №1​

[T] is an unconditionally-Copy, non-owning borrowed view. It has no Drop; using a view does not move the underlying elements out of the view.

Referenced by: rfc-0061, rfc-0071, rfc-0126, rfc-0171

Tested by (5)
Legality Rule №2​

An array index expression must have type u64.

Referenced by: rfc-0007

Fixed-size arrays​

SinceFixed-size arraysv0.8.0

[T; N] is an array type whose length N is a non-negative integer literal known at compile time. [T; N] coerces to [T] (not the reverse). N must be a non-negative integer literal; variables are not permitted.

fun main() {
// Repeat construction: every element is the same value.
let zeros: [i64; 3] := [0; 3];

// Literal construction with an explicit sized type.
let ones: [i64; 3] := [1, 2, 3];

// Coerces to [T] when a [T] is expected.
fun first(xs: [i64]) -> i64 { xs[0] }
let v := first(ones); // [i64; 3] → [i64]
}

Indexing and for-in work identically to [T]. Array patterns match sized arrays:

fun sum(xs: [i64; 3]) -> i64 {
match (xs) {
[a, b, c] => a + b + c, // exact-count pattern on [T; 3]
}
}
Changed inv0.12.0RFC-0126unannotated array literals now have [T; N], not [T]

An unannotated literal such as [1, 2, 3] has type [i64; 3]: its length is statically known and it owns its elements. Slices arise only from borrowing, never from a literal. The [T; N] → [T] coercion above applies wherever [T] is expected — a let/var target, a function argument, or a generic instantiation — so existing call sites need not change when they already accept a [T; N]-typed or explicitly [T]-annotated value. Only an unannotated literal's own type changed.

See the note under "Arrays" above — this split is not considered final.

Formal rules
Legality Rule №1​

An array literal has fixed-size-array type [T; N], not [T], where N is its literal element count.

Referenced by: rfc-0126

Tested by (18)
Legality Rule №2​

[T; N] implicitly coerces to [T] wherever [T] is expected. The reverse coercion is not permitted.

Referenced by: rfc-0053, rfc-0126

Tested by (5)
Legality Rule №3​

[T; N] is a fixed-size-array type only when N is a non-negative integer literal; the element type and literal length both participate in type identity, including for [T; 0].

Referenced by: rfc-0053

Tested by (4)
Dynamic Semantics №1​

A repeat array expression [expr; N] evaluates expr once, then clones that result to produce all N elements.

Referenced by: rfc-0053

Legality Rule №4​

Where [T; N] is expected, an array literal is accepted only when it contains exactly N elements of type T.

Referenced by: rfc-0053

Tested by (4)
Legality Rule №5​

A fixed-size array type [T; N] is valid as a struct field type.

Referenced by: rfc-0053

Legality Rule №6​

A fixed-size array may have another fixed-size array as its element type, such as [[i64; 2]; 2].

Referenced by: rfc-0053

Legality Rule №7​

An exact array pattern for a [T; N] value must have a compatible element count; a different exact count is rejected.

Referenced by: rfc-0053

Tested by (3)
Legality Rule №8​

The length in [T; N] is an integer literal, not a named generic type parameter or an arbitrary runtime expression.

Referenced by: rfc-0053

Legality Rule №9​

Every literal index into [T; 0] is statically rejected because it is out of bounds.

Referenced by: rfc-0053

References​

Sincev0.10.0

Reference types provide explicit aliasing.

fun main() -> i64 {
var value := 1;
let p: &i64 := &value;
let q: &var i64 := &var value;
*q := *p + 1;
return *q;
}

Metel has two reference types:

  • &T — shared immutable reference to T
  • &var T — exclusive mutable reference to T
GapGAP-OWNERSHIP-001#1293RFC-0122RFC-0067Nothing prevents a shared and an exclusive reference to the same place from coexisting.

"Exclusive" means shared XOR exclusive: a place may have any number of &T borrows, or exactly one &var T, never both.

&var T coerces to &T. The reverse coercion does not exist. Both are non-owning aliases — a reference never owns the value it points to.

&T is Copy; &var T is not, so an exclusive reference is moved on use rather than duplicated. Passing one as an argument reborrows instead of moving — see §References and moves.

References are first-class values, but they are distinct from the referent type. Ordinary access — field reads/writes, indexing, method dispatch, reading a plain value out — goes through auto-deref (selectors only); the explicit dereference operator *p (v0.11.0) reads through a reference and writes through a &var T (*p = v). See §References.

&var accepts arbitrary addressable lvalue paths — struct fields, tuple elements, array elements, and chains thereof. Writes through the resulting &var T propagate back to the original storage location:

struct Counter { value: i64 }

fun main() -> i64 {
var c := Counter { value = 0 };
let p: &var i64 := &var c.value;
*p := 42;
return c.value; // 42
}
Since&var for lvalue pathsv0.10.0
Formal rules
Legality Rule №1​

An &var T reference may be used where &T is expected; an &T reference may not be used where &var T is expected.

Referenced by: rfc-0067a

Reading a value out of a reference​

Reading the value a reference points to is always spelled *r. This cannot be a move (references never own their referent), only a copy, and only when the referent's type permits copying:

fun main() -> i64 {
let x := 42;
let r: &i64 := &x;
let y: i64 := *r; // explicit read: copies the referent out
return y;
}

A reference is never implicitly converted to its referent, at any position: not a let or var annotation, a call argument, a return value, a break value, the tail expression of a body, an if/else branch, a match arm, or an ascription. Each is a type error, T0001, whose help line names the fix:

fun get(r: &i64) -> i64 {
r // error[T0001]: expected i64, found &i64
// = help: read the value out with `*r` (copies a Copy referent)
}
fun bump(p: &var i64) -> i64 {
*p += 1;
*p // tail expression: explicit read
}

A chain of reference layers is read one * per layer:

fun main() -> i64 {
let x := 42;
let r: &i64 := &x;
let rr: &&i64 := &r;
let y: i64 := **rr; // one `*` for each layer
return y;
}

&var T still coerces to &T where a shared reference is expected (spec.types.references.legality-1L1). A non-Copy T cannot be read out of a reference: *r of a non-Copy referent is a move out of a reference, rejected by the move checker (see §Ownership).

Formal rules
Legality Rule №1​

A reference-typed expression is never implicitly converted to its referent type. Where a non-reference type is expected, a reference-typed expression is a type error; *r reads the referent out, one * per reference layer, and only a Copy referent can be read out.

Referenced by: rfc-0067a, rfc-0185

Tested by (10)

List<T>​

Sincev0.8.0

List<T> is the standard growable-sequence type. Use it when you need to append, pop, or otherwise mutate a sequence. Use [T] when the sequence is a borrowed view after construction.

fun main() {
var xs: List<i64> := List::new();
xs.push(1);
xs.push(2);
xs.push(3);
println(xs.len().to_string()); // 3
let last := xs.pop(); // Some { value = 3 }
}

Construction:

FormDescription
List::new()Empty list
List::from(arr)Construct from a [T] — copies elements

Methods:

MethodSignatureDescription
push(&var self, value: T)Append an element
pop(&var self) -> Option<T>Remove and return the last element, or None
len(&self) -> i64Number of elements
get(&self, index: i64) -> Option<T>Bounds-checked access
as_slice(&self) -> [T]View as an immutable array (no copy)

List<T> does not implicitly coerce to [T]. Call .as_slice() to get a read-only view.

Changed inv0.12.0RFC-0126as_slice returns a live borrowed view rather than a copied result

as_slice returns the same underlying storage, and the result remains a view for as long as it is used, bounded by self's lifetime. "No copy" therefore describes the value's full lifetime, not only the call itself.

Formal rules
Dynamic Semantics №1​

List::new() creates an empty list, and List::from(source) creates a list containing the elements of source.

Referenced by: rfc-0054

Dynamic Semantics №2​

push appends an element; pop removes and returns the last element, or None for an empty list.

Referenced by: rfc-0054

Dynamic Semantics №3​

len reports the list's current number of elements, including changes made by push and pop.

Referenced by: rfc-0054

Dynamic Semantics №4​

get(i) returns Some for an in-bounds element and None when i is out of bounds.

Referenced by: rfc-0054

Legality Rule №1​

A List<T> is distinct from [T]; obtaining its array view requires an explicit .as_slice() call.

Referenced by: rfc-0054

Type Ascription​

Sincev0.2.0

The : operator asserts that an expression has a given typeL1. It is a type-inference hint that also supplies the expected type for unsizing coercions; it never reads through a reference.

Type ascription is mainly an ergonomics feature. Most code should type-check from its surrounding context alone; : is for the cases where spelling out the intended type inline is clearer than introducing a separate annotated binding.

fun main() -> i64 {
let xs := [] : [i64];
let x := 1 : i64;
if (xs.len() == 0) { x } else { 0 }
}

Ascription fails at compile time if the inferred type of the sub-expression cannot be unified with the ascribed typeL2. For example, 1 : String is invalid. Use as to convert between types; use : only when the value already has the target type.

fun main() -> i64 {
let y := 1 : String;
return 0;
}
Formal rules
Legality Rule №1​

expr : T constrains expr to type T and supplies T as its expected type. It never reads a value out of a reference; it may apply the unsizing coercions that an expected type already drives (dyn erasure, [T; N] to [T], singleton types).

Referenced by: rfc-0021, rfc-0023

Tested by (3)
Legality Rule №2​

An ascription is valid only when the expression's type unifies with the ascribed type; otherwise it is a type error.

Referenced by: rfc-0021

Legality Rule №3​

An expression may contain at most one type ascription; a second : in the same ascription position is a parse error.

Referenced by: rfc-0021

When ascription helps​

Type inference uses surrounding expected types. That expected type can come from a let annotation, a function return type, a callee's parameter types, or the surrounding expression context.

Because of that, ambiguous literals like [] and None often type-check without explicit ascription when the context already determines their type:

fun zip_lengths(a: [i64], b: [String]) -> i64 {
return a.len() + b.len();
}

fun make_row(use_default: boolean, fallback: [i64]) -> [i64] {
return match (use_default) {
true => [],
false => fallback,
};
}

fun first_or_default(items: [i64], fallback: Option<i64>) -> i64 {
return match (fallback) {
Some { value } => value,
None => if (items.len() > 0) { items[0] } else { 0 },
};
}

fun main() -> i64 {
let total := zip_lengths([], ["a", "b"]);
let built := make_row(true, [1, 2, 3]);
let first := first_or_default([1, 2, 3], None);
return total + built.len() + first;
}

Ascription is still useful when no surrounding context fixes the type:

fun main() -> i64 {
let arr := [] : [i64];
let value := None : Option<i64>;
match (value) {
Some { value } => value + arr.len(),
None => arr.len(),
}
}

Without such context, ambiguous literals remain a type error. For example, let x = None; does not provide enough information to infer the element type.

fun main() -> i64 {
let x := None;
return 0;
}

Type Casting​

The as operator performs an explicit conversion from expr's type to TD1. It desugars to a call to the From aspect and is infallible — the result is the target type directly.

fun main() {
let x: i32 := 1000i32;
let b: i8 := x as i8; // wraps: 1000 mod 256 → -24
let f: f32 := x as f32; // 1000.0f32
let u: u64 := x as u64; // 1000u64

let pi: f64 := 3.14;
let n: i32 := pi as i32; // truncates toward zero → 3
}

All pairwise casts among i8, i16, i32, i64, u8, u16, u32, u64, f32, f64 are supported. Narrowing integer casts wrap (two's-complement truncation). f64-to-integer casts truncate toward zero.

Because as desugars to From, user-defined types become castable by implementing From<SourceType> for the target type.

Formal rules
Dynamic Semantics №1​

expr as T evaluates an explicit conversion of expr to T via From<S>::from (where S is expr's type) and produces a value of type T. Not restricted to numeric types — any type with an applicable From<S> implementation is a valid cast target.

Referenced by: rfc-0021

Generics​

SinceBuilt-in generic types (Option<T>, Result<T, E>, [T])v0.1.0User-defined generic functions and typesv0.3.0

Types and functions can be parameterized with <T> syntax.

struct Stack<T> {
items: [T],
}

fun first<T>(arr: [T]) -> Option<T> {
if (arr.len() == 0) {
return None;
}
return Some { value = arr[0] };
}

fun main() -> i64 {
let stack := Stack { items = [1, 2, 3] };
match (first(stack.items)) {
Some { value } => value,
None => 0,
}
}

Rigid type parameters​

GapGAP-TYPES-005An operator such as + or < on a bare type parameter is rejected, and no aspect exists that grants it.

A declared type parameter is opaque inside its own definition: it is equal only to itself, and supports only what its declared bounds entail. A generic definition is accepted or rejected on that basis alone, never by trying it against particular arguments.

fun pick<T>(a: T) -> i64 { a } // error: expected i64, found T
fun id2<T, U>(a: T) -> U { a } // error: expected U, found T

The declared bounds (inline T: A, where clauses, and the enclosing extend's bounds) are the whole contract: a use is allowed when the bounds entail it, and is an error at the definition otherwise.

Formal rules
Legality Rule №1​

Inside a generic function or method, each declared type parameter, including row parameters and the parameters of the enclosing extend, is equal only to itself or to a type that the definition's declared bounds state is equal to it. Unifying it with a concrete type, or with another declared parameter, in any other way is a type error (T0001) at the definition. The declared bounds state equalities in exactly two places: a row equation (where R = { label: Type, ..Rest }, in which Rest is derived from R and lacks the named labels, so R never equals Rest) and an associated-type binding (T: Iterable<Item = X> makes T::Item equal to X). A closure or nested generic function inside the body sees the enclosing parameters as rigid; a recursive generic call instantiates the callee's parameters afresh.

Tested by (20)
Legality Rule №2​

A body may use a parameter T only in ways its declared bounds Γ entail, by membership in Γ and with no transitive closure: a method call x.m() iff some bound in Γ(T) declares m; a field access x.label on a row-bounded parameter iff a row bound in Γ names label, at the type that bound declares; a method of a conditional impl on a type built from T iff the impl's conditions are entailed by Γ. A use not granted by Γ is T0035. Passing x where a callee requires U: A is entailed iff A ∈ Γ(T), and otherwise is T0012. where all R: A grants A to the fields of R, not to R, and is forwarded only to another where all R: A bound. T::Assoc is an opaque projection equal only to itself or to what an associated-type binding in Γ declares. A bare parameter is not callable. Impl resolution for a type built from T assumes Γ.

Tested by (15)
Legality Rule №3​

An operator on a bare type parameter (a + b over T) is rejected (T0005): no bound grants it, because no aspect grants an operator yet. T: Eq grants == through its eq method.

Dynamic Semantics №1​

A well-formed generic definition is accepted or rejected independently of any call. An implementation may still check a body again with concrete types, but that re-check never accepts a definition the rules above reject, and a body the definition check accepted cannot be rejected by it: if it is, the two checks disagree, which is the internal error §I0010 generic definition and…, never a diagnostic about the call site.

Tested by (4)

Row bounds​

Sincev0.12.0

A bound written as a row accepts any type carrying at least the listed fields:

fun squared_magnitude<record T: { x: f64, y: f64, .. }>(p: T) -> f64 {
p.x * p.x + p.y * p.y
}

The trailing .. is load-bearing. It stands for "and a rest I am not naming," and its presence is what makes the bound open:

fun g<record T: { x: f64 }>(p: T) // closed: T's row is exactly `x`
fun h<record T: { x: f64, .. }>(p: T) // open: T has at least `x`

A record pattern's own trailing .. reads the bound's listed fields the same way field accessL6 does, and — unlike field access — can discard the rest of an open bound's unlisted fields rather than being unable to name them at all:

fun describe<record T: { x: f64, .. }>(p: T) -> String {
match (p) {
{ x, .. } => "x is ${x}, plus whatever else the caller passed",
}
}

The .. is required to match an open bound at all — its full field set isn't known here, so a pattern that doesn't end in .. can never be exhaustive:

fun bad<record T: { x: f64, .. }>(p: T) -> f64 {
match (p) {
{ x } => x, // error: open bound's field set isn't known here; add `..`
}
}

A closed bound's fields are fully known, so .. there is optional sugar rather than a requirement — a pattern matching a closed bound must still name every field the bound lists unless it uses ..:

fun get_x<record T: { x: f64, y: f64 }>(p: T) -> f64 {
match (p) {
{ x, y } => x, // OK: every field of the closed bound is named
// { x } => x, // error: `y` isn't named and there's no `..`
}
}

Naming a field the bound doesn't list is still rejected, .. or not — the pattern's rest form discards unnamed fields, not fields the bound never promised are there:

fun bad2<record T: { x: f64, .. }>(p: T) -> f64 {
match (p) {
{ x, z, .. } => x, // error: no field `z` on the bound
}
}

A field may omit its type to constrain the label only — { x } means "carries an x, whatever its type":

fun f<record T: { x, .. }>(p: T) // has an `x` of some type
fun g<record T: { x, y: f64, .. }>(p: T) // any-typed `x`, `f64` `y`

Negation reuses the ! that bounds already accept, and is the complement of the positive bound — just as !Copy means "does not implement Copy". It takes no .., since absence has no rest to quantify over:

fun send<record T: !{ token }>(t: T) -> i64 { … } // carries no `token` at all
fun tag<record T: !{ id: String }>(t: T) -> i64 { … } // no `String`-typed `id`

Note the second form is satisfied by a record whose id is an i64 — it does not have a String id. Write !{ id } for "no id of any type".

A row bound is satisfied by a record, not by a nominal struct. The record marker on the type parameter says so at the declaration; a bare <T: { … }> is an error.

The marker may be written at the parameter or in a where clause — the two are equivalent, and a parameter is record-kinded if either one carries it:

fun f<record T: { x: f64, .. }>(p: T) -> f64
fun g<T>(p: T) -> f64 where record T: { x: f64, .. }

The row bound is optional. <record T> on its own means "any record, whatever its fields" — the only way to write that, since a bound of { .. } alone is not accepted:

fun labels<record T>(x: T) -> [Symbol] // any record; no constraint on its fields
squared_magnitude({ x = 3.0, y = 4.0 }); // a record — satisfies the bound
squared_magnitude(some_point); // a struct — does not

Nominal structs do not satisfy row bounds.

Why row capability is opt-in​

A nominal type's API is what it declares. An anonymous record's API is what it contains.

Once a type satisfies row bounds, its field names and types are part of its public interface, whether the author intended that or not. Renaming a field breaks every caller who wrote a bound mentioning it; adding one can make the type accidentally satisfy a bound its author never heard of. On a struct, a field rename is an internal change.

That is why structural capability is opt-in rather than automatic:

encapsulationstructural flexibility
structlayout is private; the API is what you declarenone

Most types want the first. A value whose shape is genuinely the contract — a coordinate pair or a configuration fragment — can use an anonymous record.

What satisfies which bound​

Both bound kinds are opted into; they differ only in granularity. An aspect bound is opted into per aspect, by writing an implementation. A row bound is opted into per type, by choosing the record kind. Nothing is implicit in either direction.

Sincev0.13.0RFC-0137metel-core#857a plain struct's row is not visible to row-bound satisfaction

Every struct is represented internally as (brand, row) (see Ownership — Narrowing§Narrowing). The table's "no" is a visibility gate, not the absence of a row: a plain struct's row is never visible to row-bound satisfaction, regardless of narrowing or projection — including at full width, where its content is identical to a same-shaped record's. This preserves the same observable outcome as before, expressed on the branded-row mechanism itself.

non-local aspect (Display)local aspectrow bound
structyes, with an implyes, with an implno
enumyes, with an implyes, with an implno — sums, not products
anonymous recordno — see belowyes, with an implyes

An anonymous record has no owning module, so the orphan rule permits an implementation only for an aspect local to the implementing module. Every standard-library aspect is non-local, which means no anonymous record is Display and println("${r}") does not work on one. Auto-derived aspects are unaffected — Send and Sync are computed from field composition rather than declared.

Open rows​

Available in v0.14.0. RFC-0121 open rows support row extension, decomposition, reusable type positions, and row-conditional implementations.

GapGAP-TYPES-008RFC-0178No form combines the fields of two rows into one record: merging two generic records has no spelling.

A row-kinded generic parameter abstracts over "the rest of a row" rather than a concrete shape. The binder is row R; every use of the variable is written ..R, distinguishing it from a bare type variable in every position a row can appear — a record type's tail ({ x: f64, ..R }), a struct's own residual projection (Handle.{ ..R }), and a generic argument (Session<..R>):

fun get_x<row R>(p: { x: f64, ..R }) -> f64 { p.x }

Extension needs no operator — the new label goes directly in the row literal:

RequestBuilder<{ ..R, auth: String }>

Removal from an abstract row uses a rest pattern. A where equation names both halves and states how they compose, which simultaneously subsumes a row bound (R = { token, ..Rest } already implies R: { token, .. }) and names the remainder:

extend<row R, row Rest> Session<..R>
where R = { token: Token, ..Rest }
{
fun authenticate(self) -> Session<..Rest> { ... }
}

The equation constrains the type. In a generic body, { token, ..rest } consumes a record and binds its unnamed fields as an owned anonymous record. A row spread ({ ..rest, auth = a }) places an owned record's fields into a new record. The details and current implementation limit are specified below.

Reading a field the equation promises is:

fun split<row R, row Rest>(p: { token: i64, ..R }) -> i64
where R = { extra: i64, ..Rest }
{
p.token + p.extra
}

fun main() {
assert(split({ token = 1, extra = 2, other = 3 }) == 3);
}

For a rest pattern, the checker removes the explicitly named labels from the strongest decomposition entailed by the scrutinee's type and declared row facts. Other known fields and the open tail remain. Thus, if S = { token: Token, extra: i64, ..Rest }, then { token, ..rest } gives rest: { extra: i64, ..Rest }. A presence-only bound such as R: { token, .. } proves that token exists but does not determine the remainder type; the pattern is rejected with a distinct “remainder type cannot be determined” diagnostic. If all fields are removed from a closed row, the remainder has empty anonymous-record type {}, not Unit. Bare .. in a pattern remains discard-only and does not produce a value.

A record literal accepts at most one spread, including for concrete rows. Written labels must be proven absent from the spread row; collisions do not override. Initializers evaluate from left to right, while field order/layout is determined by the resulting row/type, not initializer order.

Legality Rule №4​

A record literal contains at most one spread; each explicitly initialized label must be entailed absent from the spread row, and an absent-label fact is required for abstract rows. An absent-label fact is a closed row without the label, a decomposition whose remainder excludes it, or a label-only negative bound (R: !{ token }). A typed negative bound such as R: !{ token: String } is not one: it only rules out that type, and R may still hold the label at another type. A collision is an error, not an override. Spreading an owned record moves its fields into the result and consumes the source. Spreading through a reference copies the fields only when every field is provably Copy; otherwise it is rejected with T0033. Initializers evaluate left-to-right, while the resulting record layout is determined by its row/type.

A row variable may also be the argument of a nominal type's own row parameter, written ..R in the argument list. A bound on that row (where R = { auth: String, .. }) is checked at each use:

record Builder<row R> { name: String, data: { ..R } }

fun auth_of<row R>(b: Builder<..R>) -> String where R = { auth: String, .. } {
b.data.auth
}

fun main() {
let b := Builder { name = "req", data = { auth = "tok", retries = 3 } };
assert(auth_of(b) == "tok");
}

An argument whose row lacks a required label is rejected with T0012:

record Builder<row R> { name: String, data: { ..R } }

fun auth_of<row R>(b: Builder<..R>) -> String where R = { auth: String, .. } {
b.data.auth
}

fun main() {
let bare := Builder { name = "bare", data = { retries = 1 } };
let _ := auth_of(bare);
}

A tail-only parameter { ..R } transfers the entire owned row, and a parameter that names fields ({ f: T, ..R }) takes the whole record too, so the two spellings accept the same programs and an owned remainder may hold non-Copy fields.

Width subtyping. A record with extra fields may be passed to a parameter that names only some of them ({ x: f64, ..R }); the extras bind to R. By value the parameter takes the whole record: the callee owns the extra fields and may move, return or drop them, so they need not be Copy:

fun get_x<row R>(p: { x: i64, ..R }) -> i64 { p.x }

fun main() {
let wide := { x = 1, name = "ada".to_string() };
assert(get_x(wide) == 1);
}

Taking the record by reference does not move it, so the caller keeps every field:

fun get_x<row R>(p: &{ x: i64, ..R }) -> i64 { p.x }

fun main() {
let wide := { x = 1, name = "ada" };
assert(get_x(&wide) == 1);
assert(wide.name == "ada");
}

Inside a generic body the extras are abstract, and the body owns them too, so it can pass the parameter on by value with no bound. where all R: Copy is only needed to use the row more than once:

fun consume<row R>(p: { x: i64, ..R }) -> i64 { p.x }

fun outer<row R>(s: { x: i64, ..R }) -> i64 {
consume(s) + 1
}

fun main() {
assert(outer({ x = 1, k = "kept".to_string(), flag = true }) == 2);
}

An open record with an anonymous tail ({ x: T, .. }) is not accepted everywhere; see legality-5L5, as decided by RFC-0181. Three limits are recorded as gaps.

GapGAP-TYPES-010RFC-0181acceptedAn open record with an anonymous tail is rejected as a return type, so an API cannot promise only some fields of the record it returns.

A function cannot return an open record while hiding its extra fields: an anonymous tail in a return type is T0032. Omit the return type, write the record in full, or name a row that comes from an argument.

GapGAP-TYPES-011RFC-0181acceptedAn open record with an anonymous tail is rejected inside a function type, and a named row fixes the row for the whole signature, so no written type means 'works for every wider record'.

A written function type cannot say it accepts any record with at least some fields. A named row (|{ x: T, ..R }| -> U) works for callbacks over records of one shape.

GapGAP-TYPES-012RFC-0181acceptedAn anonymous open record is rejected as a field type, so a field cannot hold 'any record with at least these fields'.

A record or struct field cannot have an open-record type with an anonymous tail; give the record a row parameter instead.

Formal rules

Referenced by: rfc-0178

Tested by (13)
Legality Rule №1​

A generic parameter may be declared row R (parallel to record T); every use of a row variable outside its own binder is written ..R (or anonymous ..) — in a record type's tail, a struct's own residual-projection tail, or generic-argument position. A bare identifier in any of these type positions is a type variable, never a row; row and type variables are therefore never ambiguous with each other.

An open record type may occur in a reusable type position, including a function or instance-method parameter, a return type, a local annotation, or a nominal field. Its row tail is substituted and checked wherever the enclosing type is instantiated.

A struct's residual-projection tail (Handle.{ fd, ..R }, Handle.{ ..R }) is narrower: it is accepted only as the type of a function parameter, and anywhere else (a return type, an annotation, a field, under &) it is a parse error naming that restriction. A row argument to a nominal type (Session<..R>), an open record, or a closed projection (Handle.{ fd }) covers the same ground in those positions. A projection that names no fields (Handle.{ ..R }) takes a whole struct, like { ..R }, and so does one that names fields (Handle.{ fd, ..R }); see legality-3L3.

A row argument is a row variable (..R), an anonymous .., or a literal row: an open or closed record type written after ... Req<..{ host: String, ..R }> is Req at the row R extended with host; Req<..{ host: String }> is Req at exactly that one field, and Req<..{}> at the empty row. A literal row is the same type the checker infers for a record literal with a spread, so a written return type and an inferred one agree. The labels it adds must be provably absent from the tail, exactly as for a spread (R: !{ host }), and it is accepted only where the generic parameter is declared row. An anonymous tail inside a literal follows the positions in legality-5L5.

record Req<row R> { data: { ..R } }

fun empty() -> Req<..{}> { Req { data = {} } }

fun add_host<row R: !{ host }>(r: Req<..R>, h: String) -> Req<..{ host: String, ..R }> {
Req { data = { host = h, ..r.data } }
}

extend<row R: { host: String, .. }> Req<..R> {
fun build(self) -> String { "GET " + self.data.host }
}

fun main() {
assert(add_host(empty(), "a.com").build() == "GET a.com");
}

Referenced by: rfc-0121, rfc-0180

Tested by (30)
Legality Rule №2​

A row literal's trailing ..R (or anonymous ..) extends that row with whatever fields R carries. A where R = { label: Type, ..Rest } equation decomposes a row variable R into one named field and a remainder Rest, and simultaneously bounds R to carry at least that field (equivalent to R: { label: Type, .. }).

Referenced by: rfc-0121

Tested by (12)
Legality Rule №3​

A by-value parameter whose type has an open row tail ({ x: Type, ..R }, or Handle.{ fd, ..R }) takes its argument whole. The call needs only that the argument has the named fields at the declared types; the fields the parameter does not name are the callee's to move, return or drop, so they need not be Copy, and a generic body that forwards such a parameter to another by-value open-row function needs no where all R: Copy. A parameter taken through a reference (&{ x: Type, ..R }, &var) never moves the value and forgets nothing.

A nominal argument whose type has a Drop impl is the one exception: dropping it inside the callee needs drop dispatch against a narrowed residual (RFC-0137 §5, #949), which is not implemented, so passing it to an open-row parameter is rejected with T0036. A plain record that only contains a Drop value is unaffected.

Referenced by: rfc-0121, rfc-0179

Tested by (20)
Legality Rule №5​

An open record type whose tail is anonymous, { x: T, .. } or { .. }, denotes a record with at least the named fields and a remainder that is a fresh row at each occurrence. It is accepted:

  • as the type of a function or method parameter;
  • as the annotation of a let or var binding;
  • as the annotation of a closure parameter; and
  • as the right-hand side of a type alias, where each use of the alias is its own fresh row.

In a let or var annotation the fresh row is solved by the initializer, so the binding has the initializer's type and every one of its fields stays readable; the annotation names the fields that must be present and does not narrow the value. The field types written in the annotation apply to the initializer, so { x: u8, .. } makes x a u8. A closure whose parameter carries such an annotation is polymorphic in that row: each call instantiates it afresh, so one closure accepts records of different shapes.

fun main() {
let a: { x: i64, .. } := { x = 1, y = "s".to_string() };
assert(a.y == "s");

let get := |p: { x: i64, .. }| { p.x };
assert(get({ x = 1, y = 2 }) == 1);
assert(get({ x = 5 }) == 5);
assert(get({ x = 7, flag = true }) == 7);
}

An open record with an anonymous tail is rejected with T0032 as a return type, inside a written function type, and as a record or struct field. In a return type or function type the tail has no construct that solves it, and a field's type would differ between values of one nominal type. A named tail ({ x: T, ..R }, R declared row) is not affected by this rule and is accepted wherever R is in scope.

fun make() -> { x: i64, .. } {
{ x = 1, y = 2 }
}

fun main() {
let _ := make();
}

Referenced by: rfc-0181

Tested by (13)

Field-wise row constraints​

A constraint applies an aspect bound to every field of a row, rather than to the row's own type as a whole — distinct from a row bound (R: { x: f64, .. }), which constrains the row's shape (which labels it has), not its contents (what the field types can do):

extend<row R> { ..R }: Display where all R: Display { … }
fun consume<row R>(r: { ..R }) where all R: Copy { … }

all R: A holds when every field type in R satisfies A; it holds vacuously on an empty row. A per-field exception composes through ordinary row decomposition (where R = { x: T, ..Rest } where T: SomeOtherBound, all Rest: A) rather than needing a heterogeneous form of its own.

R must be a row the declaration can walk: the tail of an open-row parameter ({ ..R }), the remainder of a where decomposition, the tail of an extend target (extend<row R> { ..R }), or a declared row generic used as the row argument of a named type (Packet<..R>), in a function signature or an extend target. A row that none of these positions mentions has no fields to constrain and is rejected.

aspect Mark { fun mark(&self) -> i64; }
record Packet<row R> { data: { ..R } }
fun duplicate<row R>(p: Packet<..R>) -> ({ ..R }, { ..R }) where all R: Copy {
let d := p.data;
(d, d)
}
extend<row R> Packet<..R>: Mark where all R: Copy { fun mark(&self) -> i64 { 1 } }

fun main() {
let p := Packet { data = { a = 1 } };
assert(p.mark() == 1);
let pair := duplicate(p);
assert(pair.0.a + pair.1.a == 2);
}
Formal rules
Legality Rule №1​

where all R: Aspect is a WhereConstraint alternative, sibling to an ordinary bound (ident : BoundList) and a row equation (ident = Type, §Open rows above): it holds exactly when every field type in row R satisfies Aspect, vacuously on an empty row. It constrains R's field contents; it does not by itself constrain which labels R has (that is an ordinary row bound, composed separately).

Referenced by: rfc-0123

Tested by (29)

Implementing an aspect for a record​

Three forms, with different rules:

extend { x: f64, y: f64 }: MyAspect { … } // one concrete row
extend<row R: { x: f64, .. }> { ..R }: MyAspect { … } // every row of a given shape
extend<row R> { ..R }: MyAspect { … } // every row

All three are permitted once the aspect is local to the implementing module: the first is exactly one structural type, the others a row condition on the receiver. The receiver matches an impl when its row meets the condition, whether it is an anonymous record or a nominal §Records. Methods are checked per call against the concrete receiver, as for every structural target, and the aspect's default methods are inherited as for any impl. The field types a target writes must be spelled out: one that names an impl type parameter (extend<T> { w: T }: A) is rejected, since no receiver's field type could equal it.

The second also needs overlap checking between row bounds — two shape-conditional implementations can be incomparable rather than one being more specific, so they must be disjoint, which is specified below. The third is most useful with a way to require an aspect of every field in the row (§Field wise row constraints above, all R: Aspect), which a record-target impl accepts as where all R: Aspect. The standard library implements Display for every record of Display fields this way (its body is a built-in, LIMIT-TYPES-003) and Copy for every record of Copy fields (a bodyless impl): such a record is copied, not moved.

Two shape-conditional impls are disjoint when some label one requires the other forbids:

aspect Describe { fun describe(&self) -> String; }

extend<row R: { name: String, .. }> { ..R }: Describe {
fun describe(&self) -> String { "named ${self.name}" }
}
extend<row R: !{ name }> { ..R }: Describe {
fun describe(&self) -> String { "anonymous" }
}

fun main() {
assert({ name = "ada", age = 36 }.describe() == "named ada");
assert({ age = 36 }.describe() == "anonymous");
}

The impl is chosen from the value's current row, so a partial move changes it:

record Handle { id: i64, name: String }

aspect Describe { fun state(&self) -> String; }

extend<row R: !{ name }> { ..R }: Describe {
fun state(&self) -> String { "no name" }
}
extend<row R: { name: String, .. }> { ..R }: Describe {
fun state(&self) -> String { "has name" }
}

fun main() {
var h := Handle { id = 1, name = "secret" };
assert(h.state() == "has name");
let name := h.name; // h : Handle.{ id }
assert(h.state() == "no name");
assert(name == "secret");
}

Two impls whose conditions do not exclude each other overlap and are rejected with T0015:

aspect Describe { fun describe(&self) -> String; }

extend<row R: { name: String, .. }> { ..R }: Describe {
fun describe(&self) -> String { "a" }
}
extend<row R: { id: i64, .. }> { ..R }: Describe {
fun describe(&self) -> String { "b" }
}
Formal rules
Legality Rule №1​

A row-conditional impl (extend<row R: { ... }> { ..R }: Aspect or extend<row R> { ..R }: Aspect) resolves against a value's current row, not only its widest possible one — for a struct, that is the row of its current residual (narrower after a partial move, wider again after the moved field is restored), exactly the same current-row behavior Named Records§Records states for record's own row-bound satisfaction.

Referenced by: rfc-0121

Tested by (14)
Legality Rule №2​

When a value's brand has an ordinary (brand-keyed) impl of an aspect and its row also satisfies a row-conditional impl of the same aspect, the brand-keyed impl is selected: brand-exact dispatch is checked first, and a match there short-circuits row-conditional resolution entirely rather than conflicting with it.

Tested by (2)
Legality Rule №3​

Two row-conditional impls of the same aspect do not conflict when some field name both conditions mention is required present by one and absent (or incompatibly typed) by the other — rows being open means no other fact can correlate two row conditions, so this is the only way two of them can be proven disjoint. Any other pair of row-conditional impls of the same aspect overlaps, and is rejected exactly as two overlapping ordinary impls are. Inside a generic body over an unconstrained <row R>, a row-conditional method is not visible unless R's own bound entails the impl's condition.

Tested by (6)
Legality Rule №1​

A row bound requires record on its type parameter, either at the parameter declaration or in a where constraint; record without a row bound is also a legal any-record bound.

Referenced by: rfc-0118

Tested by (3)
Legality Rule №2​

A negative row bound is satisfied only when none of its named fields match; it accepts no trailing .. and a negative bound in a where clause is enforced like an inline one.

Referenced by: rfc-0118

Tested by (6)
Legality Rule №3​

A field in a row bound may omit its type, constraining the field label while accepting any field type.

Referenced by: rfc-0118

Tested by (2)
Legality Rule №4​

Only a record satisfies a row bound; a nominal struct is rejected even when it has matching fields.

Referenced by: rfc-0118

Tested by (2)
Legality Rule №5​

Brace syntax after a parameter or let annotation denotes an exact record type, while the same syntax in a generic parameter or where constraint denotes a row bound.

Referenced by: rfc-0118

Legality Rule №6​

A field a row bound lists is accessible via field access (p.x) from inside the function body; a field the bound doesn't list is not, even when a caller's concrete argument happens to carry it.

Legality Rule №7​

A record pattern's trailing .. binds only the fields it names against a row-bounded type parameter and discards the rest, the same as it does against a named struct. It is required to match an open bound at all, since the bound's full field set isn't known; for a closed bound it is optional, but the pattern must otherwise name every field the bound lists. Naming a field the bound doesn't list is rejected regardless of ...

Tested by (4)

Never Type​

Sincev0.10.0

! (Never) is the uninhabited bottom type — no value of type ! can ever be constructed. A loop with no reachable break has type !:

fun main() -> i64 {
let result: i64 := loop { break 42; };
return result;
}

return <expr>, panic(<message>), loop { } with no reachable break, and break/continue used as value expressions in loop context all have type !. If any sub-expression has type !, that sub-expression diverges before the outer expression can produce a value, so the outer expression's type is unconstrained and any type is accepted in that position.

Subtyping and coercion​

! is a subtype of every type — ! <: T for all T — so an expression of type ! coerces implicitly, with no cast, to any context expecting T. This is what makes the rule above sound: code after a diverging expression is unreachable, but still typechecks against whatever its context requires.

Match exhaustiveness​

A match whose scrutinee has type ! needs no arms — an empty match is vacuously exhaustive, since no value of type ! can ever reach it:

fun unreachable_code(x: !) -> i64 {
match x { } // exhaustive — no arms needed
}

More generally, an enum variant whose payload type is ! is uninhabited — no value of that variant can ever be constructed — and a match may omit the arm for an uninhabited variant while remaining exhaustive:

enum Foo {
A { x: i64 },
B { y: ! },
}

fun handle(f: Foo) -> i64 {
match (f) {
Foo::A { x } => x,
// Foo::B omitted — exhaustive; B is uninhabited
}
}

Inhabited-singleton coercion​

If an enum has exactly one inhabited variant (every other variant's payload is !) and that variant has exactly one field, a value of the enum type coerces implicitly to the field's type — the compiler inserts the destructuring, no explicit match required:

enum Wrapper<T> {
Present { value: T },
Absent { _: ! },
}

fun infallible() -> Wrapper<i64> { Wrapper::Present { value = 42 } }

fun main() -> i64 {
let x: i64 := infallible(); // implicit coercion via the inhabited-singleton rule
return x;
}

Result<T, !> satisfies this: Ok { value: T } is the one inhabited variant with one field, so a Result<T, !>-returning function's caller can use the result as a plain T with no match. Option<!> does not satisfy it — None is inhabited but has zero fields — so Option<!> never coerces implicitly to anything, though nothing prevents it from arising through generic instantiation.

! as a return type​

A function annotated -> ! promises never to return; every control-flow path must end in a diverging expression, checked by the compiler:

fun abort(msg: String) -> ! {
panic(msg);
}

A -> ! function containing a reachable return is a type error.

Formal rules
Legality Rule №1​

! is uninhabited: no terminating expression can construct a value of that type.

Referenced by: rfc-0078

Legality Rule №2​

! is a subtype of every type, and an expression of type ! implicitly coerces to any expected type.

Referenced by: rfc-0078

Legality Rule №3​

Code made unreachable by a diverging expression remains typechecked in its surrounding type context.

Referenced by: rfc-0078

Dynamic Semantics №1​

return, panic, a non-breaking loop, and value-position break or continue diverge and have type !; an enclosing expression cannot produce a value after such a subexpression diverges.

Referenced by: rfc-0078

Tested by (2)
Legality Rule №4​

Match exhaustiveness excludes impossible scrutinee values and uninhabited variants.

Referenced by: rfc-0078

Tested by (2)
Legality Rule №5​

A match whose scrutinee has type ! is exhaustive with no arms.

Referenced by: rfc-0078

Legality Rule №6​

An enum variant containing a ! payload is uninhabited; its match arm may be omitted or, if written, is unreachable but not rejected.

Referenced by: rfc-0078

Tested by (3)
Legality Rule №7​

An enum with exactly one inhabited, single-field variant implicitly coerces to that field's type; zero-field or multi-field inhabited variants do not receive this coercion.

Referenced by: rfc-0078

Dynamic Semantics №2​

When every arm of a match diverges, the match expression has type !.

Referenced by: rfc-0078

Legality Rule №8​

Result<T, !> has an uninhabited Err variant and therefore only an Ok value can be constructed.

Referenced by: rfc-0078

Legality Rule №9​

Result<T, !> satisfies the inhabited-singleton coercion rule and a match omitting Err is exhaustive.

Referenced by: rfc-0078

Tested by (2)
Legality Rule №10​

Option<!> has only its zero-field None variant inhabited; it does not coerce to a field type.

Referenced by: rfc-0078

Legality Rule №11​

A function declared -> ! is legal only when every reachable control-flow path diverges; a reachable ordinary return is a type error.

Referenced by: rfc-0078

Tested by (3)

Option<T>​

Option<T> is the built-in optional type. There is no null — all absence is expressed via Option<T>.

The type of None is Option<T> for some T that must be determinable from contextL1. If no context constrains T — for example, a bare let x = None with no annotation and no subsequent use that pins the element type — the program is a type error. An explicit annotation is required in that case:

Changed inv0.11.0RFC-0111None and Some are ordinary variants of Option<T>, not literals

None and Some have no special status in the grammar or the type system. They resolve exactly as Red does for a user-declared enum Colour { Red, .. } — bare where the expected type determines the enum, qualified (Option::None) anywhere. Everything said here about needing a determinable type follows from that general rule rather than from a rule about None specifically, and the same is true of Result<T, E>'s Ok/Err. See §Unqualified variant construct….

fun main() -> i64 {
let x: Option<i64> := None;
match (x) {
Some { value } => value,
None => 0,
}
}
fun main() -> i64 {
let result: Option<i64> := None;
let value: Option<i64> := Some { value = 42 };
match (value) {
Some { value } => value,
None => match (result) {
Some { value } => value,
None => 0,
},
}
}

Use match to unwrap safely:

struct User {
id: i64,
}

fun find_user(id: i64) -> Option<User> {
if (id == 1) {
return Some { value = User { id = 1 } };
}
return None;
}

fun main() -> i64 {
match (find_user(1)) {
Some { value } => value.id,
None => 0,
}
}

.yolo() unwraps, panicking if the value is None:

struct User {
id: i64,
}

fun find_user(id: i64) -> Option<User> {
if (id == 1) {
return Some { value = User { id = 1 } };
}
return None;
}

fun main() -> i64 {
let user := find_user(1).yolo();
return user.id;
}
Formal rules
Legality Rule №1​

None is the empty variant of Option<T> and is valid only where the expected type determines T; Option::None is valid wherever the qualified variant is named.

Referenced by: rfc-0020, rfc-0111

Tested by (3)
Legality Rule №2​

The built-in optional type is named Option. std::core declares no type named Perhaps: a reference to Perhaps that resolves to nothing is T0003, and its message states that Perhaps is now Option. That covers Perhaps written as a type, as a path or the leading segment of one (Perhaps::None, std::core::Perhaps), as an expression or pattern name, and in an import. A module that declares its own Perhaps is unaffected, and Option may be shadowed like Result and List (scoping-and-shadowing legality-9Variables L9).

Referenced by: rfc-0184

Tested by (10)

Result<T, E>​

Result<T, E> represents the outcome of a fallible operation:

fun divide(a: f64, b: f64) -> Result<f64, String> {
if (b == 0.0) {
return Err { error = "division by zero" };
}
return Ok { value = a / b };
}

fun main() -> i64 {
match (divide(8.0, 2.0)) {
Ok { value } => value as i64,
Err { error } => 0,
}
}

Use match to handle both cases, or The operator D2 to propagate errors.

.yolo()Panics D1 also works on Result<T, E>, panicking on Err.

Known gaps​

knownGAP-TYPES-001 — No higher-rank polymorphism. A function parameter is a monotype, so it cannot be used at two types in one body. Open record ↗knownGAP-TYPES-002 — The storage model of List<T> and its boundary with T[] and [T; N] is not specified. How a growable List<T> allocates and grows its storage, and where it ends and T[] and [T; N] begin, is not fully specified. Open record ↗knownGAP-TYPES-005 — No spelling for an operator on a generic parameter. An operator such as + or < on a bare type parameter is rejected, and no aspect exists that grants it. Open record ↗knownGAP-TYPES-008 — A record literal accepts at most one row spread. No form combines the fields of two rows into one record: merging two generic records has no spelling. Open record ↗knownGAP-TYPES-009 — Tuples have no rest pattern or spread over their numeric-label row. A tuple cannot be split into its first element and the rest, or extended, in a generic body. Open record ↗acceptedGAP-TYPES-010 — A function cannot return an open record while hiding its extra fields. An open record with an anonymous tail is rejected as a return type, so an API cannot promise only some fields of the record it returns. Open record ↗acceptedGAP-TYPES-011 — A written function type cannot say it accepts any record with at least these fields. An open record with an anonymous tail is rejected inside a function type, and a named row fixes the row for the whole signature, so no written type means 'works for every wider record'. Open record ↗acceptedGAP-TYPES-012 — A record or struct field cannot have an open-record type with an anonymous tail. An anonymous open record is rejected as a field type, so a field cannot hold 'any record with at least these fields'. Open record ↗
Resolved (3)
resolvedGAP-TYPES-003 — There is no nominal record kind, so named records are unavailable. RESOLVED: RFC-0120 (Named Records) specifies and now implements a nominal record kind that satisfies row bounds. Open record ↗resolvedGAP-TYPES-004 — A blanket row-conditional aspect impl's body cannot require an aspect of every field. RESOLVED: where all R: Aspect (RFC-0123) now specifies the per-field quantifier a blanket row-conditional impl's body needs. Open record ↗resolvedGAP-TYPES-006 — A generic body cannot construct or consume a row remainder. A where equation can type the remainder of a row, but no form lets a body take an abstract row apart or build one, so typestate over a row cannot be implemented generically. Open record ↗