Fixed width integers
Choose signed and unsigned 8, 16, 32 or 64 bit integers with checked conversion, overflow and compact native arrays.
Implemented in developmentAner provides Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, and UInt64 without imports. All belong to the scalar Number abstraction. Ordinary integer literals still infer Int64; choosing compact storage or unsigned range requires a type annotation or constructor. Availability: these additions are implemented in the current native development runtime and await a matching published binary. Existing research preview packages may not recognize these types. Check installation and release availability before running the examples.
Types and exact ranges
| Type | Packed bytes per element | Inclusive range |
|---|---|---|
Int8 | 1 | −128 to 127 |
UInt8 | 1 | 0 to 255 |
Int16 | 2 | −32,768 to 32,767 |
UInt16 | 2 | 0 to 65,535 |
Int32 | 4 | −2,147,483,648 to 2,147,483,647 |
UInt32 | 4 | 0 to 4,294,967,295 |
Int64 | 8 | −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
UInt64 | 8 | 0 to 18,446,744,073,709,551,615 |
The byte counts describe contiguous payload slots in Array, DynamicArray, and NDArray, excluding container metadata and temporary buffers. Scalars use the interpreter's tagged Value representation; selecting UInt8 for an isolated scalar does not promise a one byte allocation. Int128, UInt128, arbitrary precision integers, Rust style aliases such as i8, and wrapping/saturating arithmetic are not implemented.
let ordinary = 42
let sample: Int16 = -32000
let pixel = UInt8(255)
let large: UInt64 = 18446744073709551615
let same_large = UInt64(18446744073709551615)
print(type(ordinary)) // Int64
print(dtype(sample)) // Int16
print(type(pixel)) // UInt8
print(large == same_large) // truePositive decimal literals above 9223372036854775807 require an explicit UInt64 expected type context, such as the annotation or constructor above. An unannotated literal, Number(18446744073709551615), or a larger literal is rejected. No literal changes the default integer inference. -9223372036854775808 remains the lexical Int64 minimum; the integer token must immediately follow the minus, allowing whitespace/comments between tokens.
Checked conversions
A concrete integer can cross an annotated integer boundary automatically only when its actual value fits exactly. This includes signedness changes and narrowing, with a runtime range check before the destination write. A failed check never truncates bits, wraps, or changes an existing destination.
let source: Int64 = 200
let byte: UInt8 = source
let wider: UInt32 = byte
print(byte) // 200
print(type(wider)) // UInt32
// let invalid: Int8 = source // R1003: 200 does not fit Int8
// let negative: UInt8 = -1 // R1003The same boundaries apply to parameters, results, fields, wrapper payloads, and typed array scalar inputs. A statically Number, Float32, or Float64 expression needs an explicit integer constructor even when its current value is integral:
let numeric: Number = UInt16(200)
let byte = UInt8(numeric)
print(byte) // 200
print(Int16(3.9)) // 3
print(Int16(-3.9)) // -3Every integer constructor accepts an integer, or explicitly truncates a finite Float32/Float64 toward zero, and range checks the result. A negative Float64 cannot convert to an unsigned type, including UInt8(-0.5); it does not become an unsigned zero. Nonfinite and out of range inputs fail with R1003. Constructors do not parse Strings. cast<UInt8>(value) checks the exact retained type and never converts; cast<Number> accepts all integer types, Float32, and Float64.
Integer to Float64 conversions remain checked for exact representation when implicit. Float64(value) explicitly permits rounding. Float64 has 53 significant binary bits, so some Int64/UInt64 values require that explicit decision. Comparisons avoid rounding integers to floats first.
Arithmetic types and overflow
Integer +, -, *, %, and integer_div select a common representation from the operand types, before computing the result:
| Operands | Result representation |
|---|---|
| Same integer type | That type |
| Two signed types | Wider signed type |
| Two unsigned types | Wider unsigned type |
| Signed and unsigned | Smallest supported signed width that covers both complete type ranges |
Any signed type and UInt64 | No lossless common integer type; explicit conversion required |
For example, Int8(2) + UInt8(3) returns Int16; Int32(2) + UInt16(3) returns Int32. Ordinary literal 1 is Int64, so UInt8(250) + 1 produces Int64, while UInt8(250) + UInt8(10) overflows UInt8. Promotion does not choose a representation based on whether this particular result is small. Signed/UInt64 comparisons still work exactly.
integer_div truncates toward zero; % follows the dividend's sign. Unary minus retains the integer type: signed minimum negation and nonzero unsigned negation fail. Addition, subtraction, multiplication, negation, zero integer divisors, and signed minimum divided/remaindered by −1 are checked; failures report R0001. There is no automatic Float64 fallback or widening after overflow.
/ returns Float64 after checked exact promotion of integer values. An integer pair still needs a lossless common integer representation; convert explicitly before mixing signed types with UInt64. An operation with a statically Number operand has a Number result, except / and comparisons. Its runtime arithmetic uses the retained concrete values; % and integer_div reject a retained Float32/Float64.
Packed arrays
All eight integer types work in Array, DynamicArray, and NDArray. Number is an interface for scalar values rather than a packed array dtype.
let pixels = Array<UInt8>(size: 4, fill: 0)
pixels[0] = 255
pixels.set(1, 128)
print(dtype(pixels)) // UInt8
print(type(pixels.get(0))) // UInt8
let samples = DynamicArray<Int16>(capacity: 2)
samples.append(-100)
samples.append(200)
let matrix = NDArray<Int32>(shape: List(2, 2), fill: 3)
print(samples.sum()) // 100
print(matrix.matmul(matrix).to_list()) // List(18, 18, 18, 18)Array arithmetic requires the same dtype and retains its width. sum, dot, cumsum, scale, elementwise arithmetic, broadcasting, and matrix products check every integer intermediate in that dtype. An intermediate overflow fails even when later terms could cancel it. Index returning methods return Int64. Mean, population variance, and standard deviation return Float64 and may round; integer means use exact wide accumulation before conversion, and spread calculations first subtract integer origins to preserve nearby large value differences.
A failed native mutation preserves the destination's contents, length, capacity, and bookkeeping; a failed arithmetic operation publishes no partial result. Completed right side effects are not rolled back. Native array overflow reports R3001; a checked scalar conversion before entering a native array operation reports R1003. A persistent session runtime error still clears session state.
.to_float() explicitly allows integer rounding and returns an independent Float64 array. .to_tensor() requires Float32 or Float64 storage, so integer arrays must explicitly choose .astype("Float32"), .astype("Float64"), or .to_float() first. Floating bridges preserve the chosen dtype; see Float32. Tensor Float32/Float64 backends and precision policies retain their existing contracts. CSV schema types, Bytes accessors, and Range/temporal representation are not expanded into separate integer width APIs by this addition.
Run the complete example with aner check examples/integer_types.aner followed by aner examples/integer_types.aner.