Number
Keep concrete numeric representations behind one scalar interface, preserve exact implicit conversions and choose rounding explicitly.
Implemented in developmentNumber is an import free scalar type accepting any of the eight fixed width integer types or Float32/Float64. It preserves the concrete representation of each value. Int8/UInt8 through Int64/UInt64, and Float32/Float64, remain available when a fixed representation is part of a function or storage contract.
Availability: these rules are implemented in the current native development runtime and await a matching published binary. Existing research preview packages may reject Number or retain earlier integer division behavior. Check installation and release availability before running the complete numeric example.
Declarations and inspection
var value: Number = 42
print(type(value)) // Int64
value = 2.5
print(type(value)) // Float64
print(dtype(value)) // Float64
let inferred_integer = 42
let inferred_float = 2.5
let numeric = Number(42)Unannotated integer and floating literals still infer Int64 and Float64. Their inferred binding type stays fixed. Declaring Number, or initializing with Number(value), opts into the numeric abstraction. A Number variable may change between integer and floating representations; it cannot hold a Bool, String, physical quantity or missing value.
Number(value) accepts a numeric expression and preserves its value and representation. type, type_of and dtype expose the concrete integer type, Float32, or Float64, including through Number and Any; there is no additional scalar storage format named Number. Inspection does not narrow an expression's static type.
Parameters, fields and results
fn increment(value: Number, step: Number = 1) -> Number {
return value + step
}
fn sample(use_integer: Bool) {
if use_integer { return 7 }
return 7.5
}
print(increment(2)) // 3
print(increment(2.5)) // 3.5
print(type(sample(true))) // Int64
print(type(sample(false))) // Float64Number works in annotated bindings, parameters, returns, class fields, generic classes such as Cell<Number>, and wrapper payloads such as Optional<Number> and Result<Number, String>. Numeric literal defaults can initialize Number parameters. Functions and methods returning different concrete numeric types infer Number while preserving the returned representation. This does not infer a general union for unrelated return types or add generic functions.
Number accepts every concrete integer type, Float32, and Float64 without conversion. At a Float64 boundary, a concrete integer or an integer valued Number is automatically converted only if its integer value is exactly representable. Concrete integer boundaries allow checked, exact integer to integer conversion, including narrowing and signedness changes. This applies to annotated assignments, function arguments and returns, fields, wrapper payloads and typed array scalar inputs. Float32 boundaries permit only exactly representable integer/Float64 inputs implicitly; write Float32(value) to permit rounding. Float32 widens to Float64 exactly. A statically floating or Number expression needs an explicit integer constructor such as Int16(value); see integer ranges and conversions.
Arithmetic and comparison
| Operation | Rule |
|---|---|
+, -, * | Two concrete integers use the lossless common integer type, with checked overflow. A Float64 operand selects Float64; otherwise a Float32 operand selects Float32. Integer inputs promote only when exact in the selected precision. |
Unary - | Retains the concrete numeric representation; signed minimum and nonzero unsigned negation fail. |
/ | Always returns Float64, including 5 / 2 == 2.5. Integer operands undergo checked exact promotion first. |
integer_div(left, right) | Requires integer values and returns their common integer type, truncating the quotient toward zero without floating point conversion. |
% | Requires integer values and returns their common integer type, with the dividend's sign. |
==, !=, <, <=, >, >= | Compare typed numeric values directly, including mixed integers and floats, without rounding an integer to Float64 first. |
For +, - and *, an expression with a static Number operand has a static Number result; its concrete result follows the table. / has static Float64 result. A pair of integer operands, including for /, must have a lossless common integer type; signed/UInt64 pairs need explicit conversion, while comparisons remain supported. % and integer_div accept a statically Number operand only when its retained value is an integer. Compound assignments follow the same rules, and the result must fit the destination type. An integer or Float32 binding or array cell cannot use /=; write count = integer_div(count, 2) when an integer quotient is intended.
print(2 + 0.5) // 2.5
print(5 / 2) // 2.5
print(integer_div(-7, 3)) // -2
print(-7 % 3) // -1
print(9007199254740993 > 9007199254740992.0) // trueInteger overflow, zero integer divisors and minimum integer division/remainder by negative one report the existing integer runtime diagnostic R0001. Float32 and Float64 scalar arithmetic retain IEEE 754 behavior: ordinary scalar overflow and division by zero may produce infinity or NaN, including 1 / 0. Number does not make Float64 arithmetic exact or add arbitrary precision integers. Native APIs and numerical buffers retain their own finite value checks.
Numeric operator equality differs from Any and collection equality. Any equality and List/Dictionary/Set element or key equality retain concrete tags: UInt8(1), Int64(1), and Float64 1.0 remain distinct in those contexts. Cast a dynamically retrieved value to Number before using typed numeric operators.
Explicit precision loss
Float64 has 53 bits of significant binary precision. Every supported integer value from -9007199254740992 through 9007199254740992 converts exactly; some larger integers do too. Aner checks the actual value, rather than rejecting all values beyond that interval.
let exact: Float64 = 42
let larger_exact: Float64 = 9007199254740994
let rounded = Float64(9007199254740993)
print(rounded == 9007199254740992.0) // true
print(Int64(3.9)) // 3
print(Int64(-3.9)) // -3An implicit conversion that would change an integer reports source located R1003, before the surrounding operation or destination write completes. For example, let value: Float64 = 9007199254740993 and 9007199254740993 + 0.5 fail. Write Float64(value) explicitly to permit rounding.
Float64(value) accepts a numeric value: integers may round, and existing Float64 values are preserved. Each integer constructor, such as Int64(value) or UInt8(value), converts an integer exactly or explicitly truncates a finite Float32/Float64 toward zero, with a range check; nonfinite and out of range values fail with R1003. Unsigned constructors reject negative values, including a Float64 such as -0.5. Neither floating type converts implicitly to an integer, even for a value such as 2.0.
These checks protect conversion of the input value. Subsequent Float64 arithmetic still rounds: adding a small value to a much larger floating value may leave it unchanged, and decimal fractions such as 0.1 generally have approximate binary representations.
Casts and homogeneous storage
cast<Number>(value) checks that the retained value is a supported integer type, Float32, or Float64 without converting it. cast<Int8> through cast<UInt64>, and cast<Float32>/cast<Float64>, check their exact concrete tags; a mismatch reports R1002. Use a constructor for conversion:
let dynamic: Any = 42
let numeric = cast<Number>(dynamic)
print(type(numeric)) // Int64
print(Float64(numeric)) // 42.0Number is not an accepted Array, DynamicArray or NDArray dtype. Those buffers keep explicit homogeneous storage: Float32/Float64 scalar fill/write arguments can accept exactly representable numeric values, while integer scalar writes use checked exact conversions into the declared dtype. The integer dtypes use packed 1/2/4/8-byte slots. Array to array arithmetic requires matching dtypes. Integer intermediates are checked in the retained width; floating results must be finite. Float32 slots occupy four bytes. Explicit .astype("Float32") or .astype("Float64") selects floating result storage and permits rounding. Explicit .to_float() continues to permit integer rounding. Array/Tensor bridges preserve Float32 or Float64 dtype; Tensor device transfers and precision policies remain explicit; Number does not infer or change their precision.
Unit literals also retain their Float64 literal syntax: 5.0@m is valid after importing aner.units, while 5@m is rejected. Computed unit factories with Float64 inputs use the checked numeric boundary; physical unit conversion still requires its explicit unit API.
Scalar numeric representation checks, conversions and arithmetic use bounded O(1) time and space after evaluating their operands. Number does not promise native compilation, specialization or a performance improvement. See the language reference, type inspection and typed arrays for the surrounding contracts.