int is a 64-bit signed integer and float is a 64-bit IEEE-754 double.
Unsuffixed literals default to those two types, so most code never writes a
width at all.
fn main(): void {
count := 42 // int
ratio := 0.75 // float
println("${count} items at ${ratio}")
}int and i64 are one type, as are float and f64. The fixed-width
spellings — i8, i16, i32, i64, i128, isize, u8, u16, u32,
u64, u128, usize, f32 — exist for binary, network, and FFI boundaries.
byte is a spelling alias for u8.
Literals
Integer literals accept decimal, hex, octal, and binary bases, and _ may be
used as a digit separator anywhere inside the number. A type suffix pins the
literal to a specific width.
fn main(): void {
decimal := 1_000_000
hex := 0xFF
octal := 0o17
binary := 0b1010
mask: u32 = 0xFFFFu32
precise: f32 = 1.25f32
scientific := 1.5e3
wide: i128 = 170141183460469231731687303715884105727
println("${decimal} ${hex} ${octal} ${binary}")
println("${mask} ${precise} ${scientific} ${wide}")
}An annotated binding also fixes the width, so mask: u32 = 0xFFFF and
mask := 0xFFFFu32 mean the same thing. A literal that does not fit its target
width is rejected at check time:
fn too_wide(): u8 {
return 300u8
}Watch the interaction between - and a suffix: -128i8 parses as the negation
of the literal 128i8, and 128 does not fit in i8.
fn min_i8(): i8 {
return -128i8
}Reach the extreme through a wrapping step instead:
fn min_i8(): i8 {
return (-127i8).wrapping_sub(1i8)
}
fn main(): void {
println("${min_i8()}")
}Arithmetic wraps by default
Atoll programs cannot panic, so +, -, and * wrap to the destination width
rather than aborting. wrapping_add, wrapping_sub, and wrapping_mul are the
explicit spellings of exactly that behaviour — reach for them when wrapping is
the intent rather than the fallback.
fn main(): void {
counter: u8 = 250u8
println("${counter + 10u8}") // 4 — the operator wraps
println("${counter.wrapping_add(10u8)}") // 4 — same result, stated
println("${counter.wrapping_sub(255u8)}") // 251
println("${counter.wrapping_mul(3u8)}") // 238
}When overflow means the input was wrong, range-check before the arithmetic. The guard is a plain comparison, and the failure becomes a typed error:
error QuoteError { Overflow }
const MAX_LINE_TOTAL: int = 1_000_000_000
fn line_total(unit_price: int, quantity: int): int ! QuoteError {
if unit_price < 0 || quantity < 0 { error Overflow }
if quantity != 0 && unit_price > MAX_LINE_TOTAL / quantity { error Overflow }
return unit_price * quantity
}
fn main(): void {
match line_total(1999, 3) {
Ok(total) => println("${total}")
Err(e) => println("overflow")
}
match line_total(999_999_999, 99) {
Ok(total) => println("${total}")
Err(e) => println("overflow")
}
}Division and remainder
Integer / truncates toward zero and % returns the matching remainder, so
the remainder carries the sign of the dividend. Division by zero is the one
integer operation with no defined answer — guard the divisor.
error MathError { DivideByZero }
fn ratio(numerator: int, denominator: int): int ! MathError {
if denominator == 0 { error DivideByZero }
return numerator / denominator
}
fn main(): void {
println("${7 / 2} ${-7 / 2} ${7 % 2} ${-7 % 2}") // 3 -3 1 -1
match ratio(7, 0) {
Ok(v) => println("${v}")
Err(e) => println("undefined")
}
}Bit operations
Every integer width implements BitAnd, BitOr, BitXor, BitNot, Shl, and
Shr, spelled &, |, ^, ~, <<, and >>. Shifts on signed types are
arithmetic; on unsigned types they are logical.
fn main(): void {
a := 0b1100
b := 0b1010
println("${a & b}") // 8 — 0b1000
println("${a | b}") // 14 — 0b1110
println("${a ^ b}") // 6 — 0b0110
println("${~a}") // -13 — two's complement
println("${a << 2}") // 48
println("${a >> 2}") // 3
}A flag set is the idiomatic use. Keep the flag constants in one integer type and
combine them with |; clear with & ~; toggle with ^.
const READ: int = 0b001
const WRITE: int = 0b010
const EXEC: int = 0b100
fn has(flags: int, bit: int): bool => (flags & bit) != 0
fn describe(flags: int): string {
mut out := ""
if has(flags, READ) { out = out + "r" }
if has(flags, WRITE) { out = out + "w" }
if has(flags, EXEC) { out = out + "x" }
return if out.is_empty() { "-" } else { out }
}
fn main(): void {
all := READ | WRITE | EXEC
println(describe(all)) // rwx
println(describe(all & ~WRITE)) // rx
println(describe(all ^ READ)) // wx
println(describe(0)) // -
}Mixing widths in one bitwise expression is a type error, exactly as it is for
+. Convert first:
fn mix(a: u32, b: int): u32 {
return a & b
}Conversions
Every integer type carries to_* methods for every other width. They are
wrapping conversions: narrowing keeps the low bits, and signed/unsigned
conversions of the same width reinterpret the bit pattern.
fn main(): void {
value := 300
println("${value.to_u8()}") // 44 — low byte
println("${7i8.to_int()}") // 7
println("${value.to_float()}") // 300.0
println("${value.to_u32()}")
println("${value.to_usize()}")
println("${2.9.to_int()}") // 2 — truncates toward zero
}u8(value) and float(value) are cast-expression spellings of the same
operation, so value.to_u8() and u8(value) agree.
Because the to_* family truncates silently, a boundary that must reject
out-of-range values needs its own range check:
error HeaderError { FieldTooLarge { value: int } }
fn encode_length(value: int): u16 ! HeaderError {
if value < 0 || value > 65_535 { error FieldTooLarge { value: value } }
return value.to_u16()
}
fn main(): void {
match encode_length(1200) {
Ok(n) => println("${n}")
Err(e) => println("too large")
}
match encode_length(70_000) {
Ok(n) => println("${n}")
Err(e) => println("too large")
}
}Integer-to-float conversion can lose precision above 2^53. Float-to-integer
conversion truncates toward zero, and float.to_f32 can lose both precision
and range.
Bounds
min, max, and abs are available on integers and floats. clamp is
declared but has no lowering path — compose it from max and min.
fn clamp_percent(raw: int): int {
return raw.max(0).min(100)
}
fn spread(a: int, b: int): int {
return (a - b).abs()
}
fn main(): void {
println("${clamp_percent(140)} ${clamp_percent(-3)} ${clamp_percent(42)}")
println("${spread(4, 9)}")
}Floating point
ceil, floor, round, and truncate all return an int, not a float.
round goes half away from zero.
fn rounding(x: float): string {
up: int = x.ceil()
down: int = x.floor()
nearest: int = x.round()
toward_zero: int = x.truncate()
return "${up} ${down} ${nearest} ${toward_zero}"
}
fn main(): void {
println(rounding(2.5)) // 3 2 3 2
println(rounding(-2.5)) // -2 -3 -3 -2
}Rounding to a fixed number of decimal places is round_to in the prelude, but
that one does not lower. Scale, round, and unscale by hand:
fn round_to(value: float, places: int): float {
mut scale := 1.0
for _ in 0..places {
scale = scale * 10.0
}
return (value * scale).round().to_float() / scale
}
fn main(): void {
println("${round_to(3.14159, 2)}") // 3.14
println("${round_to(2.71828, 3)}") // 2.718
}The math surface is sqrt, cbrt, pow, abs, sign, exp, log, log2,
log10, sin, cos, tan, asin, acos, and atan. All take and return
float; angles are radians.
struct Vec2 { x: float, y: float }
fn Vec2.magnitude(self): float {
return (self.x * self.x + self.y * self.y).sqrt()
}
fn Vec2.normalized(self): Vec2 {
m := self.magnitude()
if m == 0.0 { return Vec2 { x: 0.0, y: 0.0 } }
return Vec2 { x: self.x / m, y: self.y / m }
}
fn Vec2.rotated(self, radians: float): Vec2 {
c := radians.cos()
s := radians.sin()
return Vec2 { x: self.x * c - self.y * s, y: self.x * s + self.y * c }
}
fn main(): void {
v := Vec2 { x: 3.0, y: 4.0 }
println("${v.magnitude()}") // 5
println("${v.normalized().x}") // 0.6
println("${v.rotated(1.0).y}")
println("${(8.0).cbrt()} ${(2.0).pow(10.0)} ${(8.0).log2()}")
}float.atan2(y, x) is declared as a static, not a method — y.atan2(x) is a
type error — and it has no lowering path either way.
NaN and infinity
Floating division by zero produces an IEEE infinity, and domain errors such as
the square root of a negative produce NaN. Neither is a typed error, so code
that must reject them has to test for them. is_nan and is_finite are the two
classification predicates that lower; is_infinite and is_normal do not, and
!x.is_finite() covers the first of them.
fn safe_ratio(numerator: float, denominator: float): float? {
result := numerator / denominator
if result.is_finite() { return Some(result) }
return None
}
fn classify(x: float): string {
if x.is_nan() { return "nan" }
if !x.is_finite() { return "infinite" }
if x == 0.0 { return "zero" }
return "finite"
}
fn main(): void {
println(classify(1.0 / 0.0)) // infinite
println(classify((0.0 - 1.0).sqrt())) // nan
println("${safe_ratio(1.0, 0.0) ?? -1.0}")
println("${safe_ratio(1.0, 4.0) ?? -1.0}")
}Ordinary comparison follows IEEE, where NaN is unordered — x < y, x > y,
and x == y are all false when either side is NaN. That also means a NaN used
as a Map key can never be looked up again. Filter NaN out before sorting or
keying rather than relying on total_compare, which does not lower.
fn sorted_finite(values: []float): []float {
return values.filter(v => v.is_finite()).sorted()
}
fn main(): void {
for v in sorted_finite([3.0, 1.0 / 0.0, 1.0, 2.0]) {
println("${v}")
}
}Parsing and formatting
Parsing is optional-returning and never panics. int.from_string and
float.from_string are the static forms; string.to_int and string.to_float
are the method forms, and they are the same operation.
error ConfigError { NotANumber { key: string } }
fn read_port(raw: string): int ! ConfigError {
match int.from_string(raw.trim()) {
Some(port) => return port
None => error NotANumber { key: "port" }
}
}
fn read_timeout(raw: string): int {
return raw.trim().to_int() ?? 30
}
fn main(): void {
match read_port(" 8080 ") {
Ok(p) => println("port ${p}")
Err(e) => println("bad port")
}
println("${read_timeout("junk")}") // 30
println("${"4.25".to_float() ?? 0.0}")
}Parsing rejects leading or trailing junk and out-of-range values, so "12abc"
and "99999999999999999999" both give None.
Formatting goes the other way with to_string, which every numeric type
implements. String interpolation calls it for you, so "${x}" and
x.to_string() produce the same text.
fn main(): void {
n := 1999
println("${n} == ${n.to_string()}")
println("${(0.5).to_string()}")
}to_radix(base) is the prelude’s non-decimal formatter and does not lower. A
digit table plus a mut []byte covers the same ground:
const DIGITS: string = "0123456789abcdef"
fn to_binary(value: int, width: int): string {
mut out: []byte = List.with_capacity[byte](width)
mut i := width - 1
for i >= 0 {
bit := (value >> i) & 1
out.append(DIGITS.to_bytes().get(bit.to_u32()) ?? 48u8)
i = i - 1
}
return out.decode_utf8() ?? ""
}
fn main(): void {
println(to_binary(0b1011, 8)) // 00001011
}The low-level write_decimal methods write decimal ASCII into caller-reserved
byte storage. They back Buffer.append_int and Buffer.append_float; prefer
those (see Bytes) over calling
write_decimal yourself.
fn main(): void {
mut line: []byte = []
line.append_string("count=")
line.append_int(42)
println(line.decode_utf8() ?? "")
}Generic numeric code
Arithmetic is available generically through the Numeric trait, which supplies
add, sub, mul, div, to_int, and to_float. There is no Add trait.
fn total[T: Numeric](values: []T, zero: T): T {
mut acc := zero
for v in values {
acc = acc.add(v)
}
return acc
}
fn main(): void {
println("${total([1, 2, 3], 0)} ${total([1.5, 2.5], 0.0)}")
}Comparable[T], Equatable, and Hashable are implemented for every numeric
type, so numbers work as Map keys, sort keys, and min/max arguments
without any extra declaration.
fn spread(values: []int): int {
hi := values.max() ?? 0
lo := values.min() ?? 0
return hi - lo
}
fn main(): void {
println("${spread([4, 9, 1, 7])}")
}A worked example
A latency histogram: parsing at the boundary, integer comparison for bucketing, and float math only for the reported average.
struct Bucket { label: string, count: int }
fn bucket_for(latency_ms: int): string {
if latency_ms < 10 { return "<10ms" }
if latency_ms < 100 { return "<100ms" }
if latency_ms < 1000 { return "<1s" }
return ">=1s"
}
fn parse_samples(raw: []string): []int {
return raw.map(s => s.trim().to_int() ?? -1).filter(v => v >= 0)
}
fn summarize(samples: []int): []Bucket {
labels := ["<10ms", "<100ms", "<1s", ">=1s"]
mut buckets: []Bucket = []
for label in labels {
n := samples.count(v => bucket_for(v) == label)
if n > 0 { buckets.add(Bucket { label: label, count: n }) }
}
return buckets
}
fn mean(samples: []int): float {
if samples.is_empty() { return 0.0 }
return samples.sum().to_float() / samples.len().to_float()
}
fn main(): void {
samples := parse_samples(["4", "42", "900", "1500", "not-a-number"])
for b in summarize(samples) {
println("${b.label}: ${b.count}")
}
println("mean ${mean(samples)}ms over ${samples.len()} samples")
println("p-max ${samples.max() ?? 0}ms")
}Choosing a type
Use int for counts, application-level indexes, and database integers. Use
usize for layout sizes and pointer arithmetic, and never expose a
pointer-sized width in a file or wire format — pick an explicit u32/u64
there instead. Use fixed widths at binary, network, and FFI boundaries, and
float unless a protocol specifically calls for f32.
Binary floating point is not a decimal money type. Represent fixed-scale values as an integer count of minor units:
struct Money { cents: int }
fn Money.plus(self, other: Money): Money => Money { cents: self.cents + other.cents }
fn Money.to_string(self): string {
whole := self.cents / 100
fraction := (self.cents % 100).abs()
pad := if fraction < 10 { "0" } else { "" }
return "${whole}.${pad}${fraction}"
}
fn main(): void {
total := Money { cents: 1999 }.plus(Money { cents: 501 })
println(total.to_string()) // 25.00
}Availability
These signatures are installed for semantic checking but have no lowering path
in the wasm backend today. Calling any of them compiles under atoll check and
then fails atoll build with
ATOLL2004: builtin method ... has no lowering path.
| Signature | Working substitute |
|---|---|
checked_add, checked_sub, checked_mul, checked_div |
range-check the operands first and return a typed error |
saturating_add, saturating_sub, saturating_mul |
range-check, then max/min the result |
clamp(lo, hi) (int and float) |
x.max(lo).min(hi) |
pow(n) on integers |
multiply in a for loop |
try_to_i8…try_to_u64 |
compare against the target range, then to_* |
to_radix(base) |
digit table + mut []byte, as shown above |
to_char() on int |
char.from_u32(n.to_u32()) |
round_to(places) |
scale, round, unscale |
total_compare(other) |
filter NaN out, then sorted / < |
is_infinite(), is_normal() |
!x.is_finite(), plus an explicit zero test |
float.atan2(y, x) |
no substitute yet |
int.MAX, float.PI and the other associated constants |
write the literal |
wrapping_add, wrapping_sub, and wrapping_mul do lower, on every integer
width, which is why they carry the overflow examples on this page.