Primitive types are the values the compiler understands without a user declaration. Every one of them is spelled in lowercase.
enabled: bool = true
count: int = 42
ratio: float = 0.75
letter: char = 'λ'
octet: byte = 0xFF
message: string = "ready"
println("${enabled} ${count} ${ratio} ${letter} ${octet} ${message}")Annotations are optional — the compiler still fixes one concrete primitive per binding before lowering.
enabled := true // bool
count := 42 // int
ratio := 0.75 // float
letter := 'λ' // char
message := "ready" // string
println("${enabled} ${count} ${ratio} ${letter} ${message}")Overview
| Type | Meaning | Literal |
|---|---|---|
bool |
Boolean truth value | true, false |
int |
Signed 64-bit default integer | 0, 42, -7 |
float |
IEEE 754 64-bit default float | 0.0, 1.25 |
byte |
Unsigned 8-bit integer | 0xFF, 65 in a byte context |
char |
One Unicode scalar value | 'A', 'λ' |
string |
Owned UTF-8 text | "Atoll" |
void / Unit |
Exactly one value, written () |
() |
! |
Never — control does not continue | none |
int and float are language defaults, not platform-sized aliases: they are
64 bits on every target.
Integers
Atoll has ten fixed-width integer types plus two machine-sized ones. int and
i64 name the same type, and so do byte and u8.
| Type | Bits | Range |
|---|---|---|
i8 |
8 | −128 … 127 |
i16 |
16 | −32,768 … 32,767 |
i32 |
32 | −2,147,483,648 … 2,147,483,647 |
int / i64 |
64 | −9,223,372,036,854,775,808 … 9,223,372,036,854,775,807 |
i128 |
128 | −2¹²⁷ … 2¹²⁷−1 |
byte / u8 |
8 | 0 … 255 |
u16 |
16 | 0 … 65,535 |
u32 |
32 | 0 … 4,294,967,295 |
u64 |
64 | 0 … 18,446,744,073,709,551,615 |
u128 |
128 | 0 … 2¹²⁸−1 |
usize |
target word | sizes, lengths, indices |
isize |
target word | offsets, pointer differences |
Every one of those bounds is a valid literal in its own type:
a: i8 = 127
b: i16 = 32767
c: i32 = 2147483647
d: i64 = 9223372036854775807
e: i128 = 170141183460469231731687303715884105727
f: u8 = 255
g: u16 = 65535
h: u32 = 4294967295
i: u64 = 18446744073709551615
j: u128 = 340282366920938463463374607431768211455
k: usize = 0
l: isize = -1
println("${a} ${b} ${c} ${d} ${e}")
println("${f} ${g} ${h} ${i} ${j}")
println("${k} ${l}")Literal spellings
Integer literals accept decimal, hexadecimal, octal, and binary bases, and _
as a digit separator anywhere between digits.
decimal := 1_000_000
hex := 0xDEAD_BEEF
octal := 0o755
binary := 0b1010_1010
println("${decimal} ${hex} ${octal} ${binary}") // 1000000 3735928559 493 170An unsuffixed literal is int unless an expected type says otherwise. A
suffix pins the type without needing an annotation, and works on every base.
tiny := 1u8
small := 2i8
port := 3u16
offset := 4i16
flags := 5u32
signed32 := 6i32
wide := 7u64
also_int := 8i64
huge := 9u128
signed128 := 10i128
size := 11usize
delta := 12isize
mask := 0xFFu8
nibble := 0b1111_0000u8
println("${tiny} ${small} ${port} ${offset} ${flags} ${signed32}")
println("${wide} ${also_int} ${huge} ${signed128} ${size} ${delta}")
println("${mask} ${nibble}") // 255 240Range is checked at compile time
Contextual typing checks the literal against the target’s range before any runtime value exists. It never wraps or saturates silently.
fn overflowing(): void {
a: i8 = 128
println("${a}")
}That reports ATOLL2002: integer literal 128 is out of range for type i8.
The - in -7 is unary negation applied to the literal 7, so an unsigned
target rejects it too:
fn negative_unsigned(): void {
a: u8 = -1
println("${a}")
}That compile-time guarantee applies only while the value is still a literal. Once arithmetic runs, the rules below take over.
Arithmetic wraps
Atoll programs cannot panic, so +, -, and * wrap on overflow using
two’s-complement arithmetic rather than trapping. wrapping_add and its
relatives spell that same behaviour explicitly.
max := 9223372036854775807
println("${max + 1}") // -9223372036854775808
println("${max.wrapping_add(1)}") // -9223372036854775808
small: byte = 250
println("${small.wrapping_add(10 as u8)}")// 4
signed: i8 = 127
println("${signed.wrapping_add(1i8)}") // -128Wrapping applies to unsigned types too, so subtracting past zero produces the type’s maximum:
zero: u32 = 0
println("${zero.wrapping_sub(1u32)}") // 4294967295Division truncates — and traps on zero
Integer division truncates toward zero, and % takes the sign of the dividend:
println("${7 / 2} ${7 % 2}") // 3 1
println("${-7 / 2} ${-7 % 2}") // -3 -1Integer division and remainder are the two arithmetic operations that do not wrap. Both trap — the WebAssembly instance aborts and the program stops — in exactly two situations:
| Expression | Runtime result |
|---|---|
n / 0, n % 0 |
trap: integer divide by zero |
i64 minimum / -1 (or %) |
trap: the true quotient is out of range |
A trap is not catchable: catch, ?, and ?? all operate on typed values,
and a trap never produces one. Guard the divisor yourself.
fn safe_div(n: int, d: int): int? {
if d == 0 { return None }
if d == -1 && n == -9223372036854775807 - 1 { return None }
return n / d
}
fn main(): void {
println("${safe_div(7, 2) ?? -1}") // 3
println("${safe_div(7, 0) ?? -1}") // -1
}Floating-point division has no such hazard — it produces inf, -inf, or
NaN instead:
d := 0.0
println("${10.0 / d} ${(0.0 - 10.0) / d} ${(0.0 / d).is_nan()}") // inf -inf trueChecked and saturating arithmetic are not implementable yet
Every integer type declares checked_add, checked_sub, checked_mul,
checked_div (returning T?), and saturating_add / saturating_sub /
saturating_mul (clamping at the type’s bounds). They resolve and type-check,
so atoll check accepts them — but they have no lowering, so any program that
calls one fails to build:
fn main(): void {
max := 9223372036854775807
match max.checked_add(1) {
Some(v) => println("fits: ${v}")
None => println("would overflow")
}
}That is ATOLL2004: builtin method \checked_add` has no lowering path —
declared in the prelude but never implemented`.
Written out by hand, an overflow-checked add is a comparison:
fn checked_add(a: int, b: int): int? {
max := 9223372036854775807
if b > 0 && a > max - b { return None }
if b < 0 && a < (0 - max - 1) - b { return None }
return a + b
}
fn main(): void {
println("${checked_add(2, 3) ?? -1}") // 5
println("${checked_add(9223372036854775807, 1).is_none()}")// true
}Bitwise operators
&, |, ^, ~, <<, and >> are defined for every integer type through
the BitAnd, BitOr, BitXor, BitNot, Shl, and Shr prelude traits.
println("${0b1010 & 0b0110}") // 2
println("${0b1010 | 0b0110}") // 14
println("${0b1010 ^ 0b0110}") // 12
println("${1 << 10}") // 1024
println("${1024 >> 3}") // 128
println("${~0}") // -1Methods that do lower
println("${(-5).abs()}") // 5
println("${3.min(9)}") // 3
println("${3.max(9)}") // 9
println("${7.to_float()}") // 7
println("${300.to_string()}") // 300
println("${300.to_u8()}") // 44 — narrowing truncatesChoosing a width
Use int for ordinary arithmetic and byte for octets. Reach for an explicit
width when a wire format, foreign ABI, or numeric protocol fixes it. Use
usize and isize only for sizes, offsets, and host-facing addressing — they
are target-dependent, so they do not belong in a serialized format.
struct FrameHeader {
version: u8
flags: u16
payload_len: u32
stream_id: u64
}
fn declared_size(h: FrameHeader): int {
return h.version.to_int() + h.flags.to_int()
+ h.payload_len.to_int() + h.stream_id.to_int()
}
fn main(): void {
h := FrameHeader { version: 1, flags: 0x0F, payload_len: 1024, stream_id: 7 }
println("${declared_size(h)}") // 1047
}Floats
float is 64-bit IEEE 754 and f64 is an alias for it. f32 is the 32-bit
form. A floating literal is float unless context or a suffix says otherwise.
temperature := 21.5 // float
exponent := 1.5e10 // float
pixel: f32 = 0.5
exact := 3.25f32
explicit := 1.25f64
println("${temperature} ${exponent} ${pixel} ${exact} ${explicit}")The rounding, sign, comparison, and classification methods lower directly:
x := 2.9
println("${x.floor()} ${x.ceil()} ${x.round()} ${x.truncate()}") // 2 3 3 2
println("${x.abs()} ${x.sign()} ${x.sqrt()}") // 2.9 1 1.70293863659264
println("${x.is_nan()} ${x.is_finite()}") // false true
println("${x.min(1.0)} ${x.max(1.0)}") // 1 2.9Note that floor, ceil, round, and truncate return int, not float —
they are the rounding conversions. round rounds half away from zero and
truncate cuts toward zero, so they disagree on negatives:
x := -2.9
println("${x.floor()} ${x.round()} ${x.truncate()}") // -3 -3 -2Integer and floating literals are separate categories. An integer literal is not accepted in a float position just because its value is representable:
fn category(): float {
ratio: float = 2
return ratio
}Write 2.0, or convert explicitly with to_float().
fn average(values: []int): float {
if values.len() == 0 { return 0.0 }
mut total := 0
for v in values { total = total + v }
return total.to_float() / values.len().to_float()
}
fn main(): void {
println("${average([1, 2, 6])}") // 3
println("${average([])}") // 0
}f32 and float are distinct types with no implicit widening between them;
f32.to_float() converts.
small: f32 = 1.5
wide: float = small.to_float()
println("${wide + 0.25}") // 1.75Booleans
bool has exactly two values and is the only type a condition accepts.
fn gate(enabled: bool, attempts: int, name: string): string {
if enabled && attempts < 3 { return "retry" }
if !name.is_empty() || attempts == 0 { return "greet" }
return "stop"
}
fn main(): void {
println("${gate(true, 1, "ada")} ${gate(false, 9, "")} ${gate(false, 9, "x")}")
}There is no truthiness conversion — an integer, string, collection, or option in condition position is a type error:
fn truthy(count: int): string {
if count { return "some" }
return "none"
}Compare or inspect explicitly instead:
fn checks(attempts: int, name: string, selected: string?): int {
mut score := 0
if attempts != 0 { score = score + 1 }
if !name.is_empty() { score = score + 1 }
if selected.is_some() { score = score + 1 }
return score
}
fn main(): void {
println("${checks(1, "ada", "x")} ${checks(0, "", None)}") // 3 0
}&& and || short-circuit from left to right. The bitwise operators &, |,
and ^ are also defined on bool and do not short-circuit:
println("${true & false} ${true | false} ${true ^ true} ${!true}")
// false true false falsebool also satisfies Display, so "${flag}" renders "true" or "false".
char, byte, and string
These are three different domains: one Unicode scalar value, one numeric octet, and owned UTF-8 text.
initial: char = 'A'
octet: byte = 0x41
message: string = "Atoll"
println("${initial} ${octet} ${message}") // A 65 AtollThey do not interchange. A char literal is not a byte:
fn confusion(): byte {
b: byte = 'A'
return b
}Convert explicitly. char carries codepoint conversions and classification
predicates; byte carries to_char and the numeric widenings.
c := 'A'
println("${c.to_int()} ${c.to_u32()} ${c.to_string()}") // 65 65 A
println("${'7'.is_digit()} ${'a'.is_alphabetic()} ${' '.is_whitespace()}")
b: byte = 65
println("${b.to_char()} ${b.to_int()} ${b.to_float()}") // A 65 65
match char.from_u32(0x03BBu32) {
Some(lambda) => println("${lambda}") // λ
None => println("not a scalar value")
}len counts bytes; length counts characters
string follows the Go/Rust convention: len() is the O(1) UTF-8 byte
count read from the block header, and length() is the O(n) codepoint
count. They agree on ASCII and diverge the moment text leaves it.
ascii := "hello"
accented := "héllo"
println("${ascii.len()} ${ascii.length()}") // 5 5
println("${accented.len()} ${accented.length()}") // 6 5byte_length() is a deprecated alias for len() — it also returns bytes, so
do not reach for it expecting the codepoint count.
Indexing follows the same split. char_at is codepoint-indexed, while
substring and slice are byte-indexed and return a borrowed
substring?:
accented := "héllo"
println("${accented.char_at(1) ?? '?'}") // é — codepoint index 1
match accented.substring(0, 3) {
Some(view) => println("${view.to_string()}") // hé — bytes 0..3
None => println("byte range out of bounds")
}Both return Option, so an out-of-range index is a value you handle rather
than a trap.
The common inspection methods lower and are total:
message := " Atoll Lang "
println("${message.len()} ${message.is_empty()} ${message.contains("to")}")
println("[${message.trim()}] ${message.trim().starts_with("At")}")
println("${message.to_lower_ascii()}|${message.to_upper_ascii()}|")
println("${"a,b,c".split(",").len()} ${"ab".repeat(3)}")
println("${"hello".index_of("ll")}") // Some(2) — index_of returns an Optionstring is a primitive but not a register-sized scalar — its contents are
managed storage. Ownership rules are covered in
Values.
Interpolation calls Display.to_string() on each inserted expression, so any
type with a Display implementation can appear inside ${...}:
struct Duration { millis: int }
impl Display for Duration {
fn to_string(self): string => "${self.millis}ms"
}
fn main(): void {
d := Duration { millis: 250 }
println("3 items in ${d}") // 3 items in 250ms
}Unit
void is the unit type: exactly one value, written (). Unit is an accepted
spelling of the same type, and a function may omit its return type entirely.
fn with_void(): void { println("a") }
fn with_unit(): Unit { println("b") }
fn omitted() { println("c") }
fn main(): void {
with_void()
with_unit()
omitted()
u: void = ()
v: Unit = ()
println("done")
}Unit means “completed with no payload”. It is not Option’s absence, and it is
not the never type.
Never
! is the type of an expression that does not complete normally. A function
annotated ! must never return.
fn spin(): ! {
for {}
}for { } is the bare form of Atoll’s only loop construct; there is no loop
keyword, and there is no prelude panic function to write a diverging function
with. The other way to reach ! is an expression that leaves the enclosing
function — return, error, break, or continue.
Never joins with any other type, which is why a branch that leaves the function can sit beside a branch that produces a value:
fn pick(ok: bool): int {
// The `else` arm has type `!`, so the join is `int`.
value := if ok { 1 } else { return -1 }
return value
}
fn main(): void {
println("${pick(true)} ${pick(false)}") // 1 -1
}Never is a static fact used to exclude an impossible continuation. Nothing of
type ! is ever stored at runtime.
Internal types
The compiler also knows ptr[T], rawptr[T], and the SIMD type Vec128.
They exist for unsafe runtime and code-generation interfaces — see
Unsafe — and are not application
primitives. Prefer structs, collections, and safe references in ordinary code.
Putting it together
A small unit-conversion module that touches most of the surface above: integer widths on the wire fields, float rounding for display, byte-versus-codepoint awareness in the label, and a guarded division.
struct Reading {
sensor_id: u16
celsius: float
sample_count: int
label: string
}
fn to_fahrenheit(c: float): float => c * 1.8 + 32.0
/// One decimal place, without relying on a formatter.
fn tenths(value: float): string {
scaled := (value * 10.0).round()
return "${scaled / 10}.${(scaled % 10).abs()}"
}
fn mean(total: int, count: int): float {
if count == 0 { return 0.0 } // guard: integer / 0 would trap
return total.to_float() / count.to_float()
}
fn summarize(readings: []Reading): string {
if readings.len() == 0 { return "no readings" }
mut hottest := 0.0
mut total_samples := 0
for r in readings {
f := to_fahrenheit(r.celsius)
if f > hottest { hottest = f }
total_samples = total_samples + r.sample_count
}
return "peak ${tenths(hottest)}F over ${readings.len()} sensors, "
+ "${tenths(mean(total_samples, readings.len()))} samples each"
}
fn main(): void {
readings := [
Reading { sensor_id: 1, celsius: 21.5, sample_count: 120, label: "café" },
Reading { sensor_id: 2, celsius: 30.0, sample_count: 90, label: "roof" },
]
println(summarize(readings))
first := readings.get(0) ?? Reading { sensor_id: 0, celsius: 0.0, sample_count: 0, label: "" }
println("${first.label}: ${first.len_note()}")
}
fn Reading.len_note(self): string =>
"${self.label.len()} bytes / ${self.label.length()} chars"Continue with Inference for when an annotation is required, and Conversions for moving between these types.