Skip to content

5. Types

i8 i16 i32 i64 signed integers, two's complement
u8 u16 u32 u64 unsigned integers
isize usize pointer-sized
f32 f64 IEEE-754
bool 1 byte, values true / false
char 32-bit Unicode scalar value
void the type of a function that returns nothing

Integer overflow wraps, in every build, and never panics: +, - and * on an i32 that goes past its range come back round, as they do in C, Java and C# — for signed types too, where C++ would leave it undefined. A program that wants something else says so with the methods on Integer (§14.4): a.checked_add(b) gives a T?, a.saturating_sub(b) stops at the limit. [DECIDED — §18.63]

The rest of integer arithmetic is defined too, so no operator reaches what C++ leaves undefined. Where an answer exists, it wraps; where none does, it panics, as an index out of bounds does:

Operation Result
a / 0, a % 0 panics, naming the line; a divisor that is a constant zero is an error (DZ0580)
MIN / -1 MIN: the overflow wraps, as above
MIN % -1 0
a << n, a >> n n is taken modulo the width, so 1 << 40 on an i32 is 1 << 8; a negative n is a large one
a negative value shifted left wraps, as a multiplication by a power of two would
>> on a signed value arithmetic: the sign is copied in

A float divided by zero is an infinity or NaN, as IEEE 754 says. [DECIDED — D-0094 point 5]

There are no implicit numeric conversions, in either direction, ever — not even widening. let a: i64 = some_i32 is an error; write some_i32 as i64. Untyped literals are the sole exception (§2.5). This is the single most common source of silent bugs in C++ game code and it costs nothing to forbid. [DECIDED]

Five ways to refer to something — the model your samples already use, plus &mut and ^mut.

Syntax Meaning Nullable Mutable Owns
&T shared reference no no no
&mut T exclusive reference no yes no
^T raw pointer, read-only yes no no
^mut T raw pointer yes yes no
own ^mut T owning pointer yes yes yes
  • &T / &mut T are what you pass to functions. They are guaranteed non-null and cannot be stored in a struct field, cannot be heap-allocated, and cannot be owned. A reference therefore only ever lives on the stack, so it cannot outlive its frame by being smuggled into a heap object. [DECIDED — §18.3] A view struct is the one exception D-0093 makes: it may hold views, a &T among them, and is itself placed as a reference is (§6.7). Its constructor binds a reference field in its opening statements; anywhere else, assigning to one writes through it.

    Non-null is enforced at the point of conversion, not by analysis. That the target is still alive is what ties check (§6.7).

    This is a cheap heuristic, not a proof. What the rule buys is that the most common dangling case costs zero analysis to prevent, that &T keeps a meaning stronger than “non-null pointer”, and that ties (§6.7) — which check the rest, a reference into storage a call has moved — never have to appear on a struct.

    The practical consequence: a context struct holds ^T, not &T.

    struct ButtonCtx {
    btn: ^Button // not &Button — this struct could be stored
    input: ^Input
    delta_time: f32
    }
  • ^T and ^mut T are C pointers — const T* and T*. Nullable, freely storable, no guarantees. This is how you build a scene graph with parent back-pointers, and it is on you to keep it valid. A ^T is only read through; a ^mut T may be written through as well. They stand to each other as &T and &mut T do, and as &[]T and &mut []T do: read-only unless mut is written. [DECIDED — §18.37]

  • own ^mut T is a ^mut T that this struct is responsible for releasing. It changes the type’s semantics — see §6. own ^T exists too, an owned block that is only read, but a container writes into what it owns, so own ^mut T is the usual form.

Reading through a reference. A &T or &mut T is accepted where a T is wanted, and copies the value it refers to — return m in a function returning Movie, let v: Vec3 = r, an argument, a field. Reading cannot fail, so it is not spelled out. Only a copyable T can be read this way: a move-only one would have to be moved, and nothing may be moved out of a borrow (DZ0402, §6.4), and in a generic body that means T: Copy. Where no type is wanted, a reference stays a reference: let r2 = r is another &T. [DECIDED — §18.35]

An operand of a comparison, an arithmetic or a bitwise operator is read the same way when it refers to a number, bool, char, &str or an enum: x > 2, x * 2 and x & 1 on a &i32 work on the i32, whether or not a type is wanted of the result, so a predicate taking &T reads as one taking T. A reference to a struct is compared through Eq and Ord as before (§11.2).

There is no reference to a reference. &r or &mut r on an r: &mut T refers to the T that r refers to, and is a &T or a &mut T; so is &mut list[i], whose IndexMut already hands back a reference (§11.2). &mut of a &T is an error, since nothing writable is behind it.

Writing through a reference. Assigning to a &mut T writes the T it refers to — x = 1 and x += 1 on an x: &mut i32 change the caller’s integer — since a reference cannot be pointed elsewhere. Through a &T that is an error. [DECIDED — §18.44]

Converting between them. Forming a pointer changes nothing, so a reference becomes a pointer of the same writability for free — &T → ^T, &mut T → ^mut T — and anything writable also serves where the read-only form is wanted (&mut T → ^T, ^mut T → ^T). Nothing converts the other way: a &T or a ^T never becomes a ^mut T, which would gain the right to write. Behind a ^mut the pointee must match exactly, so a ^mut ^mut T is not a ^mut ^T — through it, a read-only pointer could be stored where a writable one is expected, which is why C forbids T** → const T** too. A pointer becomes a reference through an or clause like any other fallible thing (§9.4), because a pointer may be null and a reference may not:

let btn = unsafe { ctx.btn or return } // propagate
let btn = unsafe { ctx.btn or panic!("button missing") } // assert, with a message
let btn = unsafe { ctx.btn or unreachable } // checked in debug, free in release
draw(ctx.btn) // error[DZ0522]: expected &Button, found ^Button

ctx.btn is a field with no tie, so each conversion is in unsafe; a tied pointer, as below, converts without one.

The reference it produces is as writable as the pointer: a ^mut T gives a &mut T, and a ^T gives a &T. Non-null is the only guarantee a reference carries that a pointer does not, so it is worth one line at the boundary. The check happens once, where the pointer enters scope, rather than on every function entry downstream. [DECIDED — §18.16, §18.37]

Using a pointer is unsafe. Nothing can check that a ^T still points at something, so the operations that assume it does are allowed only inside an unsafe block, or in the body of an unsafe fn. Dereference is postfix ^, and . reaches through a pointer on its own — both count:

let p: ^mut Player = &mut world.players[0] // forming a pointer: safe
if p != null { ... } // comparing, copying, storing, passing: safe
let pl = p or return // the checked conversion: safe
pl.health -= 10 // through the reference: safe
unsafe {
p.health -= 10 // `.` through a `^mut T`
let h = p^ // explicit deref, copies the Player
}

Unsafe, and only these: p^, . and [i] through a ^T, a pointer cast (§10.6), construct, destroy, move_out, memcpy and memmove (§14.3), and calling an unsafe fn. Everything else stays safe — forming, copying, storing, comparing and passing pointers, and the or conversion of a tied pointer, whose null check is the one thing that can be checked.

An untied pointer becomes a reference only in unsafe (D-0093 point 11, T-0114). A pointer is tied when it was made from a place, as p above is, or a safe call returned it under a tie, in or elided — xs.first() or return stays safe, since the callee’s body is held to that tie. A pointer parameter, a pointer field with no tie, a cast and C’s pointers are untied: ctx.btn or return is DZ0418, and unsafe { ctx.btn or return } is the programmer’s word for it. if let &b = ctx.btn is the same conversion, in every binding form, and if let &b = unsafe { ctx.btn } vouches for it. So a context struct’s ^mut World needs unsafe to use, which is honest: that aliasing is what §6.7 cannot check. A function may not hand an untied pointer out under a tie it cannot back, return ctx.world from -> ^mut World in ctx (DZ0419); return unsafe { ctx.world } vouches for it. With this, everything unchecked is written unsafe.

An unsafe fn is one whose callers must uphold something the compiler cannot see; its body is an unsafe block, and calling it needs one:

unsafe fn poke(p: ^mut i32, v: i32) { p^ = v }
unsafe { poke(slot, 3) }

unsafe marks raw memory; it is not a proof. What happens inside it is vouched for rather than checked, so a mistake there can break the promise that safe code has no undefined behaviour (§1.1). It relaxes no other rule: moving out of p^ is still DZ0402 inside unsafe. What it buys is that every place a dangling pointer can bite is marked, and that a container’s raw-memory work sits behind the safe interface it offers. Pointers are the escape hatch, not the everyday tool: shared ownership is the standard library’s Rc<T>, sole ownership of a heap value Box<T>, and a reference that notices its target is gone Weak<T> (§14.5). [DECIDED — §18.16]

null is a value of every pointer type, ^T and ^mut T alike. There is no null &T.

Auto-referencing. Method receivers are auto-referenced: buf.resize(8) works and passes &mut buf when resize takes &mut self. Free functions are not auto-referenced — you write resize(&mut buf, 8) — so that reading a call site tells you whether the callee can modify the argument. [PROPOSED]

[8]f32 fixed-size array; a value type, copied on assignment
&[]f32 slice: { ptr: ^f32, len: usize }; a read-only view, never owns
&mut []f32 the same view, through which elements may be written
const &[]f32 a slice of elements in static storage, such as reflection's tables
^[]f32 a slice that may be absent: the same view, which may be null
[N]T N may be any compile-time constant expression

A slice is borrowed (D-0093, T-0110): it views elements something else owns, so it is written as a reference and lives only where one does, as text does (DZ0517 in a field, a container or a type argument, outside a view struct). A bare []T, []mut T, const []T and &[]mut T are DZ0577, which dz fix respells. A const &[]T is no view and may be stored; for now only the compiler makes one, for reflection’s tables.

A slice carries its own writability, exactly as a reference does. &[]T and &mut []T stand to each other as &T and &mut T do (§5.2): the elements of a &[]T cannot be assigned or &mut-borrowed no matter how the slice itself is bound, and a &mut []T satisfies a &[]T parameter while the reverse is rejected. Both are two words wide and identical at runtime; the distinction is entirely in the type.

The alternative — one []T whose writability follows the binding that holds it — was rejected because a slice is a view, so the binding says nothing about the elements: a fn fill(dst: &[]f32) could then never be written, and there would be no spelling for “you may read these and not write them”, which is the whole reason &/&mut exists. [DECIDED — §18.22]

Indexing an array or slice is bounds-checked in every build, release included: it is safe code, and an out-of-range buf[i] panics with the file, line, index and length rather than reading past the end. A loop over 0..xs.len pays nothing for it, since the optimiser proves the check redundant. Where it does cost, the unchecked read is spelled out: xs.ptr is the first element as a pointer, and indexing that is pointer arithmetic, inside unsafe. .ptr is as writable as its view: a []mut T or a writable array gives a ^mut T, and a []T or a read-only array a ^T (§5.2). [DECIDED — §18.37]

Indexing a ^T is pointer arithmetic, as in C: p[i] is the ith T after p, and it is never checked, because a pointer carries no length to check against — which is why it is unsafe (§5.2). This is what a container’s m_data[i] over its own ^mut T is (§8.3). A ^T is not a sequence, so for x in p remains an error — make a slice when you want one.

An array literal is a bracketed list, and its type is an array of exactly as many elements as it lists:

let a = [1, 2, 3] // [3]i32 — the length comes from the literal
let b: [4]f32 = [1, 2, 3, 4] // the elements take the annotation's element type
let grid = [[1, 2], [3, 4]] // [2][2]i32
let empty: [0]i32 = []

Three rules, each of which exists to keep the literal readable rather than clever:

  • The length is exactly what is written. let a: [8]i32 = [1, 2] is an error naming both lengths. “The rest is zero” is already spelled let mut a: [8]i32, since §6.2 initialises every value — so a short literal would be a second way to say something the language already says, at the price of the length living only in the annotation.
  • There is no repeat form. A zeroed array is let mut a: [1024]f32 and needs no syntax; a non-zero fill is a loop or the stdlib’s fill. A [0; 8] spelling is in any case not available: §2.2 makes ; a terminator and suppresses terminators after [, so it could not be told apart from a line break inside the literal.
  • A literal may not be stored as a slice. let s: &[]i32 = [1, 2, 3] is rejected: the array is a temporary, so the slice would point at storage that is already gone. Passing one is fine — f([1, 2, 3]) on an fn f(xs: &[]i32) — because the temporary outlives the call. This is not lifetime analysis; it is one rule about one certain mistake.

The element type comes from the annotation when there is one, so [4]f32 = [1, 2, 3, 4] gives four f32s rather than four i32s converted (there are no implicit conversions — §5.1). With no annotation the elements must agree, and an untyped literal defaults as §2.5 says. [DECIDED — §18.24]

&str UTF-8 text borrowed from what owns it: { ptr: ^u8, len: usize }. Never
owns. Not null-terminated. Immutable.
const &str text whose bytes are in static storage: a literal's, a constant's.
^str text that may be absent: the same { ptr, len }, which may be null.
String stdlib: owning, growable. Written in Dazzle, not builtin.
&cstr a null-terminated C string borrowed from what owns it, for C interop.
const &cstr a literal's C string, in static storage.
^cstr C's own pointer to a C string: untied, and it may be null.

Text is borrowed (D-0093): it points into memory a String or a literal owns, so it is written as a reference, &str, and lives only where a reference does — a local, a parameter, a result — never in a field, a container or a type argument (DZ0517), except inside a view struct (§6.7). A bare str, str? for ^str, const str for const &str and &mut str are DZ0575, which dz fix respells. Text to keep is a String, or const &str when it is static. ^str’s empty case is null, and or gives the &str back. A String passed where a &str is wanted is its text, borrowed for the call (§7.2). An implementation for text is written for &str: impl Format for &str.

A slice of part of a sequence is written with a range: xs[a..b], xs[a..], xs[..b]. On an array or a slice both ends are checked when it runs, as an index is; on a pointer it is unsafe and unchecked, and the end must be written, since a pointer carries no length. The slice is as writable as what it views — a []mut T, a ^mut T, or an array that may be written gives a []mut T. [DECIDED — §18.50]

A &str is { ptr, len }, and s.len and s.ptr read them. Its bytes are s as []u8. The way back is str::from_utf8(bytes), a ^str that is null when the bytes are not well-formed UTF-8; bytes as &str skips the check and is unsafe, as is p as &cstr for a ^u8. [DECIDED — §18.50]

A C string is borrowed too (D-0093, T-0112), in three forms that are all a const char* to C++: &cstr views what owns it, a String say, and is tied to it as any reference is, so it is a view (DZ0517 in a field); const &cstr is a literal’s, which never dies and may be stored; ^cstr is C’s own pointer, untied and possibly null — what a C function returns, and what a C const char* parameter takes, from any of the three. or turns a ^cstr into a &cstr, and null empties one. A bare cstr, const cstr, &mut cstr and ^mut cstr are DZ0578, which dz fix respells.

String literals live in static storage, with a zero after the last byte. A string literal is a C string wherever one is wanted — a parameter, an annotated let, a field — and costs nothing: C++ sees the literal itself, so rl::InitWindow(1280, 720, "Dazzle") is the call it looks like. "x" as &cstr says the same explicitly. The other way, c as &str measures a C string and copies nothing; measuring C’s ^cstr is unsafe, since C owns what it points at and it may be null. A runtime &str cannot become a &cstr: nothing follows its last byte, so a C string needs a copy, and a copy has to be owned and freed. That owning C string is a String, which keeps a zero after its bytes and frees them when dropped: String(name) copies, and its as_cstr() borrows the copy as a &cstr (§14.4); a String passed as an argument where a C string is wanted is borrowed that way on its own (§7.2). s.as_str() borrows the text. [DECIDED — §18.39, D-0081, D-0089]

A const &str is text that never dies: its bytes are in static storage, a literal’s or a constant’s. The constness is the reference’s, since what it points at never dies. It is the same { ptr, len } as a &str and the same C++ type, and reads as a &str wherever a const &str is not wanted — a call, a comparison, {}, .len — so every implementation for &str is one for const &str. A &str never becomes a const &str, since it may view a String. A string literal is a const &str wherever one is wanted, as it is a const &cstr; elsewhere it is a &str, so let mut label = "none" can later take any text, while a let with no type keeps a const &str value’s type. Because nothing kills it, a const &str carries no tie (§6.7) and is no view: it can be returned from anything, held in a field and put in a container, List<const &str>, where a &str may not.

pub fn name(&self) -> const &str {
switch self {
case Idle { return "idle" }
case Walking { return "walking" }
}
}
pub struct Enemy { pub kind: const &str }
let was = enemy.state.name()
enemy.state = State::Idle{} // `was` is still valid: it points into nothing that dies

Only &str may follow const: const i32 is DZ0569. [DECIDED — D-0092, D-0093]

Because ? binds to the type name, &[]T? is a slice of optionals. A slice that may be absent is the slice pointer, ^[]T, which may be null, as text that may be absent is ^str (D-0093, T-0110).

(i32, f32) tuple
T? optional — see §5.6; built in, not a library type
fn(i32, f32) -> bool function pointer

Tuples exist mainly for multiple return values; they have positional access t.0, t.1 and destructuring let (x, y) = point(). [PROPOSED]

Every type answers a set of queries via ::, evaluated during checking and usable in if conditions that the compiler then folds away:

T::size usize, like sizeof(T)
T::align usize
T::name str, the source-level name
T::is_trivially_copyable bool — no drop, no copy, all fields trivially copyable
T::is_copyable bool — has an implicit or explicit copy
T::has_drop bool
T::fields a compile-time sequence of the fields — deferred (§12.11, §13.4)

sizeof(T) and alignof(T) are accepted as aliases for T::size / T::align, because they read better in allocation code, which is where they almost always appear.

Dazzle has no layout model of its own: T::size and T::align are what the target’s C++ compiler says, for a struct laid out as §7.3 describes.

The type sigils divide on two axes, and keeping them apart is what stops them composing into unreadable stacks.

Axis Spelled Asks
Access ^T ^mut T &T &mut T own ^mut T &[]T &mut []T how do I reach this value?
Responsibility own who frees it?
Modality T? is the value there at all?

Access is prefix, modality is postfix, and they never compete for the same position:

T? an optional T
^T a pointer, which may be null
^T? a pointer to an optional T
&T? a reference to an optional T; dereferences to T?
[]T? a slice of optionals
own ^T? an owning pointer to an optional T

? applies to value types only. There is no (&T)? and no (^T)?, because a pointer already encodes absence — ^T is the optional pointer, and layering ? on it would give you two spellings for one type. This is why the niche optimisation that earlier drafts promised for ?^T no longer appears anywhere: it has nothing to optimise. [DECIDED]

A generic result may still be an optional of a projection, T::Item?, whose instance is a reference: a list’s iterator has Item = &i32. Such a result is the pointer an optional reference would be, ^i32 (^mut i32 for &mut i32), at the call, tied as the reference would be, so last(xs.iter()) or return gives the &i32 back. Inside the generic body it is an optional as for every other instance (D-0093). Written out, (&T)? is DZ0576, which dz fix respells as ^T; text that may be absent is ^str (§5.3).

Fallibility is not on this table because it is not a property of a value. See §9.