The Built-In Types
Zero gives you a small, regular set of primitive types. Nothing exotic, nothing surprising — just the ones every systems language needs, named consistently.
| Family | Types | Notes |
|---|---|---|
| Signed integers | i8, i16, i32, i64 | Two's complement. |
| Unsigned integers | u8, u16, u32, u64 | 0 and positive only. |
| Pointer-sized | usize, isize | Width matches the platform pointer. |
| Floats | f32, f64 | IEEE-754. |
| Boolean | bool | true or false. |
| Character | char | A single Unicode scalar. |
| String | String | UTF-8 string. |
| Empty | Void | "No useful value." |
That's the full list of primitives you'll touch day to day. Compound types — shapes, enums, choices — are built from these.
Integers
The integer types follow a uniform naming pattern: i for signed, u for unsigned, followed by the bit width. So i32 is a 32-bit signed integer; u8 is an unsigned byte; i64 is a 64-bit signed integer.
let small_signed: i8 = -120
let byte: u8 = 250
let id: i32 = 1
let big: i64 = 9_000_000_000
let index: usize = 0
The default for an unsuffixed literal is i32 unless the surrounding context forces something else:
let answer = 42 // i32
When you need a specific width, attach a suffix to the literal or annotate the binding:
let byte = 250_u8 // typed literal
let byte: u8 = 250 // typed binding
Both forms produce the same value. The literal-suffix form is handy when you're passing a literal directly to a function or building a struct:
let pair: BytePair = Pair { left: 1_u8, right: 2_u8 }
When to pick which width
A short rule of thumb:
i32for most signed math. Wide enough for almost anything you'd count, fast on every platform.u8for byte-level work. Bytes from a file, bytes in a buffer, bytes over the network.u32/u64for non-negative counts when range matters. File offsets above 2 GB, large counts.usizefor sizes and indices. Pointer-sized — matches what the platform uses for memory addressing.i64for time-since-epoch and similar. Big enough for nanoseconds for hundreds of years.
Picking the smallest type that fits is good practice; picking too small a type and overflowing is a much bigger problem than picking one bit too wide.
Booleans
let ok = true
let done: bool = false
bool has exactly two values: true and false. They're literals, not constants you import from somewhere. The condition in an if or while is a bool — there's no implicit truthiness for integers or strings.
if ok {
check world.out.write("yes\n")
} else {
check world.out.write("no\n")
}
If/else covers conditionals in detail.
Floats
f32 and f64 are 32-bit and 64-bit IEEE-754 floating-point numbers respectively. Use them when you need fractional values — measurements, ratios, geometry. For exact arithmetic on currency, prefer integers in the smallest unit (cents, satoshis) over floats.
let ratio: f32 = 0.5
let pi: f64 = 3.141592653589793
f64 is the default for unsuffixed float literals.
Characters and Strings
A char holds a single Unicode scalar value:
let initial: char = 'Z'
A String is a sequence of characters, typically encoded as UTF-8 by the standard library. String literals use double quotes:
let message: String = "hello from zero\n"
Escape sequences you'd expect work — \n for newline, \t for tab, \\ for a literal backslash, \" for a literal double quote.
let multi_line = "line one\nline two\n"
The standard library exposes byte-level views over a string for low-level work. The std.mem.span("zero") form returns a Span<u8> over the bytes — useful when you're parsing, hashing, or comparing bytewise.
Void
Void is Zero's "no useful return value" type. Functions that exist for their side effects use it:
pub fun main(world: World) -> Void raises {
check world.out.write("hello\n")
}
main writes something and returns. There's no value to hand back, so the type is Void. You'll see Void on most functions that touch World — they're chosen for their effect, not their result.
Underscores in Number Literals
Long number literals can use underscores as visual separators. The compiler ignores them, so they're a pure readability feature:
let big = 9_000_000_000_i64
let bytes = 1_048_576_u32 // 1 MiB
Drop them anywhere the digits are getting hard to count.
Typed Literal Suffixes Cheat Sheet
| Suffix | Type | Example |
|---|---|---|
_i8 / _i16 / _i32 / _i64 | Signed integer | 127_i8 |
_u8 / _u16 / _u32 / _u64 | Unsigned integer | 255_u8 |
_usize / _isize | Pointer-sized | 0_usize |
_f32 / _f64 | Float | 0.5_f32 |
Reach for these when you're constructing a value where the surrounding context doesn't pin the type down.
Next: Functions
Primitives are useless without something to do with them. The next doc covers functions in Zero — how you declare them, return values from them, and bring them together to build real programs.
Frequently Asked Questions
What primitive types does Zero have?
Zero ships with sized signed integers i8, i16, i32, i64; unsigned integers u8, u16, u32, u64; pointer-sized integers usize and isize; floats f32 and f64; bool; char; String; and Void for functions that return nothing useful.
What is the default integer type in Zero?
An unsuffixed integer literal like 42 defaults to i32 unless context forces a different type. To use a specific width, write the literal with a suffix like 42_u8 or 42_i64, or annotate the binding's type explicitly with let count: u8 = 42.
Does Zero have a separate string type?
Yes. String literals like "hello" have a built-in string type that the standard library treats as a sequence of bytes (often UTF-8). For lower-level byte work, the standard library exposes spans and byte-level utilities; for character-level operations there's char for individual scalar values.
What does Void mean in Zero?
Void is the return type of a function that doesn't produce a useful value — it exists only for its side effects. The conventional pub fun main(world: World) -> Void raises signature uses Void because main exists to do I/O and exit, not to produce a value.
What is the difference between i32 and u32 in Zero?
i32 is a signed 32-bit integer with the range −2,147,483,648 to 2,147,483,647. u32 is unsigned and has the range 0 to 4,294,967,295. Use signed types when negative values are meaningful, unsigned when negative values would be a bug — for counts, indices, sizes, and so on.