Browse documentation
Language

Float32

Use explicit scalar precision, four byte packed arrays and independent Tensor bridges that preserve their declared datatype.

Implemented in development

Float32 is an import free IEEE 754 binary32 scalar type and a supported Number representation. It also selects four byte contiguous slots in Array, DynamicArray, and NDArray. Availability: these additions are implemented in the current native development runtime and await a matching published binary. The existing research preview may not recognize them. Check installation and release availability before running the example; publishing this guide does not publish a native package. Ordinary integer and floating literals still infer Int64 and Float64; Aner does not automatically choose smaller storage from a value's magnitude.

Binary32 has 24 significant binary bits, compared with Float64's 53. All integers from −16,777,216 through 16,777,216 are exactly representable in Float32; some larger integers are exact too. Most decimal fractions are approximate. The largest finite magnitude is approximately 3.4028234663852886e38.

Exact boundaries and explicit rounding

An implicit conversion to Float32 checks the actual value. An exactly representable integer or Float64 value can cross an annotated boundary; a conversion that would change the value fails with R1003. Float32 widens exactly to Float64. These rules apply to assignments, parameters, returns, fields, wrapper payloads, and packed array scalar inputs.

aner
let default_float = 0.5
let exact: Float32 = 0.5
let exact_integer: Float32 = 42
let rounded = Float32(0.1)
let wider: Float64 = rounded
print(type(default_float)) // Float64
print(type(exact)) // Float32
print(type(exact_integer)) // Float32
print(type(wider)) // Float64
print(Float64(rounded) == 0.1) // false
// let inexact: Float32 = 0.1 // R1003: implicit precision loss

Float32(value) explicitly permits rounding, including underflow of a small finite value to zero. It rejects a finite value that would round to infinity. Integer conversion rounds directly from the exact integer rather than through Float64. Float64(value) keeps its explicit integer rounding behavior and widens Float32 exactly. Integer constructors also accept Float32 explicitly, truncate toward zero, and check their integer range; no float to integer conversion is implicit.

Number(value) preserves Float32. type, type_of, and dtype report the concrete representation; cast<Float32> checks that exact tag without conversion. cast<Number> accepts all eight integer types, Float32, and Float64. Typed comparisons compare integer and floating values without rounding an integer first. Any/collection equality retains concrete tags, so Float32 1.0, Float64 1.0, and Int64 1 are distinct collection keys.

Scalar arithmetic

For +, -, and *, two Float32 operands produce Float32. Integer/Float32 arithmetic produces Float32 after checking exact integer promotion. A Float64 operand selects Float64 and widens Float32 exactly. A statically Number operand gives a Number result whose retained representation follows these same rules. Existing integer only arithmetic retains its lossless common integer type and overflow checks.

/ continues to return Float64, including for two Float32 operands. A Float32 destination therefore rejects /=; explicitly convert the quotient when Float32 rounding is intended. % and integer_div require integer values. Unary minus preserves Float32.

Ordinary scalar Float32 arithmetic follows IEEE behavior: rounding, underflow, infinity, and NaN can result. Float32(3e38) * Float32(2.0) produces infinity. Packed numerical buffers have a stricter finite value contract and reject the corresponding nonfinite write or kernel result. Float32 does not promise identical numerical results across every device or an automatic speed improvement.

Packed arrays and explicit dtype conversion

aner
let samples = Array<Float32>(size: 3, fill: 0.5)
samples[1] = Float32(0.1)
print(dtype(samples)) // Float32
print(type(samples.sum())) // Float32
print(type(samples.mean())) // Float64
let precise = Array<Float64>(size: 3, fill: 0.1)
let compact = precise.astype("Float32")
let widened = compact.astype("Float64")
print(dtype(compact)) // Float32
print(dtype(widened)) // Float64
print(dtype(compact.to_float())) // Float64

Each Float32 packed slot occupies four bytes: fixed/NDArray storage charges the element count, while DynamicArray charges reserved capacity, including unused slots. The counts exclude metadata and temporary buffers. Isolated scalars and heterogeneous collections retain the interpreter's tagged Value representation; a Float32 scalar is not a promise of a four byte allocation.

All primitive numeric dtypes provide .astype("Float32") and .astype("Float64"). The dtype argument must be a literal String so the checker can determine the result type. Conversion is explicit, detached, and preserves the array family and shape; dynamic results have compact capacity equal to their live length. Integer or Float64 values may round when converted to Float32. Float64 conversion may round large integers. Nonfinite values cannot enter a packed result. .to_float() remains an explicit conversion to Float64. Physical unit buffers require .values() before a raw numeric dtype conversion.

Float32 array operands require the same dtype; there is no array to array implicit precision promotion. Elementwise arithmetic, scale, broadcasting, cumulative sums, and matrix products produce Float32 arrays. Sum, dot, min, and max return Float32 scalars. Sum, dot, and matrix products accumulate in Float64 before a checked, rounded Float32 result; elementwise operations and cumulative sums use Float32 arithmetic. Mean, population variance, and standard deviation return Float64. All packed inputs/results must be finite; a nonfinite result or final Float32 overflow fails with R3001. Ordinary rounding, including underflow to zero, is allowed during these floating kernels.

Failed native mutation preserves contents, length, capacity, and bookkeeping; failed arithmetic publishes no partial result. A scalar precision check performed before entering a native array method reports R1003. Completed right side effects are not rolled back. Persistent session runtime failures retain their existing session reset behavior.

Dtype preserving Tensor bridges

aner
import aner.nn
let matrix = NDArray<Float32>(shape: List(2, 2), fill: 1.5)
let packed_tensor = matrix.to_tensor()
print(packed_tensor.dtype()) // Float32
let flat = Array.from_tensor<Float32>(packed_tensor)
let growable = DynamicArray.from_tensor<Float32>(packed_tensor)
let shaped = NDArray.from_tensor<Float32>(packed_tensor)
print(dtype(flat)) // Float32
print(growable.len()) // 4
print(shaped.shape()) // List(2, 2)
let wide = Array.from_tensor<Float64>(packed_tensor.astype("Float64"))
print(dtype(wide)) // Float64

Float32 and Float64 Array.to_tensor(rows, cols) and DynamicArray.to_tensor(rows, cols) preserve dtype. Dimensions must be positive and multiply to the live length. The NDArray bridge also preserves dtype and requires rank two with positive dimensions. These forward bridges create independent CPU Tensor storage with no differentiation graph. Integer arrays must explicitly convert to a floating dtype first.

Array.from_tensor<T>(tensor), DynamicArray.from_tensor<T>(tensor), and NDArray.from_tensor<T>(tensor) require an explicit T of Float32 or Float64 and an exact match with the Tensor's actual dtype. A mismatch fails with R3001; use Tensor .astype(...) explicitly before crossing the boundary. Array/DynamicArray flatten logical row major values; DynamicArray starts with live length equal to capacity. NDArray preserves the Tensor's logical shape of rank 1 to 8, subject to normal dimension, element count, and array heap limits. GPU sources may require a host readback. Every reverse result owns an independent packed copy and retains no differentiation graph or shared Tensor storage.

CPU Float32 Tensors now keep an authoritative packed float buffer, with four payload bytes per owned capacity slot. The native Float32 factory and Float32 array bridges do not create a persistent widened double buffer. A later host accessor that needs doubles can lazily add an eight byte per element cache; the typed Float32 accessor does not create that cache. CPU Float64 retains eight byte values. Graph nodes, gradients, optimizer state, provider workspace, caches, and allocator overhead remain separate from the base value payload; four bytes per cell is not a total process memory estimate.

Run the complete example. See Number, fixed width integers, Array, NDArray, and Tensor for the surrounding contracts.

Aner documentation · Reference and guides