Skip to content

Numeric Types

Almide has two default numeric types and nine sized ones:

Types
DefaultInt (64-bit signed), Float (64-bit IEEE 754)
Sized signedInt8, Int16, Int32
Sized unsignedUInt8, UInt16, UInt32, UInt64
Sized floatFloat32

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.

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 = -128
let b: UInt8 = 255
let c: UInt32 = 0xdeadbeef
let d: UInt64 = 18446744073709551615

A 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...127

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:

FromToCall
Intsizedint.to_int8(n), int.to_uint32(n), int.to_float32(n), …
sizedIntint.from_int8(x), int.from_uint32(x), …
sizedsizedx.to_int32(), x.to_uint8(), x.to_float32(), …
Floatsizedfloat.to_int8(f), float.to_uint32(f), float.to_float32(f)
Float32Floatfloat.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.

Plain narrowing wraps, in two’s complement, without trapping. When silence is not acceptable, two variants say so explicitly:

FormReturnsBehaviour
int.to_int8(n)Int8wraps
int.to_int8_checked(n)Option[Int8]none if the value would not survive the round trip
int.to_int8_saturating(n)Int8clamps 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.

Almide
main.almd
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)))
}
Press Run to compile and execute this in your browser

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.

bytes is where sized types earn their keep, with explicit endianness:

bytes.write_uint32(b, value, endian) // takes a UInt32
bytes.set_float32(b, offset, value, endian)
bytes.read_uint32(b, offset, endian) // note: returns UInt, not UInt32
bytes.read_float32(b, offset, endian) // returns Float

Two rough edges remain:

  • A sized-integer overflow the compiler can constant-fold breaks the native build (#901). let a: Int8 = 127 followed by a + b where b is 1 folds to 128 and emits an out-of-range i8 literal. Going through a function call (so the fold cannot happen) wraps correctly to -128 on both targets.
  • Mixing a sized type with plain Int is not caught. Int32 + Int64 is a clean error, but Int32 + Int type-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.

  • int and float — the full conversion surface
  • bytes — binary reads and writes
  • WebAssembly — where fixed widths matter most