Numeric Types
Almide has two default numeric types and nine sized ones:
| Types | |
|---|---|
| Default | Int (64-bit signed), Float (64-bit IEEE 754) |
| Sized signed | Int8, Int16, Int32 |
| Sized unsigned | UInt8, UInt16, UInt32, UInt64 |
| Sized float | Float32 |
Int and Float are what you want almost always. The sized types exist for
the places where width is part of the problem rather than an optimization:
binary formats, checksums, hardware and wire protocols, and interop with C or
WebAssembly.
Every sized-type module is auto-imported — int8, uint32, float32 and the
rest are available with no import.
Writing sized values
Section titled “Writing sized values”A sized value is written as an ordinary literal with a type annotation. There
is no literal suffix — 42i8 is a syntax error, since i8 parses as an
identifier.
let a: Int8 = -128let b: UInt8 = 255let c: UInt32 = 0xdeadbeeflet d: UInt64 = 18446744073709551615A literal that does not fit is a compile error, not a silent fold:
error[E024]: integer literal '200' is out of range for Int8 hint: Int8 would silently fold to 0 here; its range is -128...127Conversion is always explicit
Section titled “Conversion is always explicit”Almide has no implicit numeric widening. Mixing widths in one expression is an error that tells you what to write:
error: operator '+' mixes sized numeric types Int32 and Int64 — explicit conversion required (e.g. `.to_int32()`)Three families of conversion cover everything:
| From | To | Call |
|---|---|---|
Int | sized | int.to_int8(n), int.to_uint32(n), int.to_float32(n), … |
| sized | Int | int.from_int8(x), int.from_uint32(x), … |
| sized | sized | x.to_int32(), x.to_uint8(), x.to_float32(), … |
Float | sized | float.to_int8(f), float.to_uint32(f), float.to_float32(f) |
Float32 | Float | float.from_float32(x) |
Each sized module carries a to_* for every other numeric type plus
to_string. They hold no arithmetic: x.abs() on an Int8 is not a thing.
Narrowing: wrap, checked, or saturating
Section titled “Narrowing: wrap, checked, or saturating”Plain narrowing wraps, in two’s complement, without trapping. When silence is not acceptable, two variants say so explicitly:
| Form | Returns | Behaviour |
|---|---|---|
int.to_int8(n) | Int8 | wraps |
int.to_int8_checked(n) | Option[Int8] | none if the value would not survive the round trip |
int.to_int8_saturating(n) | Int8 | clamps to Int8’s range |
_checked follows Swift’s Int(exactly:): it is some only when the
conversion is exact. From a float that means NaN, infinity, a fractional part
and out-of-range all give none.
_saturating clamps to the nearest bound, and from a float NaN becomes 0.
fn label(name: String, o: Option[Int8]) -> String =
match o {
Some(v) => name + " = some(" + v.to_string() + ")"
None => name + " = none"
}
fn main() -> Unit = {
// Sized values are made with a type annotation — there is no `42i8` suffix.
let byte: UInt8 = 255
let word: UInt32 = 4294967295
println("UInt8 max = " + byte.to_string())
println("UInt32 max = " + word.to_string())
// Int -> sized narrowing WRAPS (it does not trap).
println("int.to_int8(300) = " + int.to_int8(300).to_string())
// _checked reports the loss instead: Option[T], none when it would not fit.
println(label("int.to_int8_checked(127)", int.to_int8_checked(127)))
println(label("int.to_int8_checked(128)", int.to_int8_checked(128)))
// _saturating clamps to the type's range.
println("int.to_int8_saturating(999) = " + int.to_int8_saturating(999).to_string())
// Float -> int truncates toward zero and saturates at the bounds.
println("float.to_int8_saturating(-3.7) = " + float.to_int8_saturating(-3.7).to_string())
println(label("float.to_int8_checked(9.5)", float.to_int8_checked(9.5)))
// Widening back to Int goes through int.from_*.
let small: Int8 = 42
println("int.from_int8(42) = " + int.to_string(int.from_int8(small)))
} Float-to-integer conversion truncates toward zero and saturates at the type’s bounds; integer-to-float rounds to nearest, ties to even.
Availability is not perfectly symmetric, for a reason: int has no
to_int64_checked, because Int is 64-bit and that conversion cannot fail.
float does have to_int64_checked and to_uint64_checked, because a float
genuinely can fall outside those ranges.
Reading and writing binary
Section titled “Reading and writing binary”bytes is where sized types earn their keep, with explicit endianness:
bytes.write_uint32(b, value, endian) // takes a UInt32bytes.set_float32(b, offset, value, endian)
bytes.read_uint32(b, offset, endian) // note: returns UInt, not UInt32bytes.read_float32(b, offset, endian) // returns FloatKnown rough edges
Section titled “Known rough edges”Two rough edges remain:
- A sized-integer overflow the compiler can constant-fold breaks the native
build (#901).
let a: Int8 = 127followed bya + bwherebis1folds to128and emits an out-of-rangei8literal. Going through a function call (so the fold cannot happen) wraps correctly to-128on both targets. - Mixing a sized type with plain
Intis not caught.Int32 + Int64is a clean error, butInt32 + Inttype-checks and then fails in the native build or produces a wrong value on wasm (#902). Convert explicitly.
Also note that Int64 and Float64 are accepted as spellings of Int and
Float in type positions, but they have no method modules — use
int.to_string(x) rather than x.to_string() on a value typed that way.
Next steps
Section titled “Next steps”- int and float — the full conversion surface
- bytes — binary reads and writes
- WebAssembly — where fixed widths matter most