Skip to content

Numbers

Integer and floating types, literal forms, wrapping arithmetic, bit operations, conversions, and math methods.

Updated View as Markdown

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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close