Refingere's numeric types say exactly how big they are, nothing converts between them without an as, and an integer calculation that overflows is an error unless it says it wraps or clamps. This page covers the numeric types, conversions, arithmetic, overflow, and the types beyond plain numbers. The Types reference lists every numeric method.
U8, U16, U32, U64, U128, U256.S8, S16, S32, S64, S128, S256.F16, F32, F64. F16 is half precision - about three significant digits, up to 65504 - for GPU data and numbers stored in bulk; calculate in F32 or F64. A whole-number literal is an Integer, which is S64, and one with a fractional part is a Float, which is F64 - unless its type is written out, or known from where it's used. See Syntax for how literals are written.
Numbers of different types never mix implicitly: adding a U8 to a U32 needs an as on one side. When the value doesn't fit the target type, as gives an OverflowError, the way overflowing arithmetic does (below); checkedCast is the same conversion with a CastError instead. A float cast to an integer drops its fraction - round(), floor() and ceil() first, for the other ways - and a NaN, an infinity or a value out of range is a CastError. Parsing text returns a ParseError for text that isn't a number.
// is floor division - which is why line comments take three slashes - and % the remainder. ^ raises to a power, and groups to the right: 2 ^ 3 ^ 2 is 2 ^ 9. Superscripts and √ write the same operations the way a formula does - see Math notation. Dividing an integer by zero is a DivisionByZeroError.
When the result of +, -, *, // or ^ on integers doesn't fit its type, it's an OverflowError, to handle or pass up with !!. A calculation that means to go past the type's bounds says which way, with an operator of its own:
+%, -% and *% wrap around, keeping the low bits - (200 as U8) +% 100 is 44.+|, -| and *| fill up to the type's bounds and stop there - (200 as U8) +| 100 is 255.a +% b is a.wrappingAdd(b) and a *| b is a.saturatingMultiply(b), so they bind like the operator they're built on, take integers of one type, and work in a compound set: set counter +%= 1. The same methods are there by name, with checkedAdd and the rest for the error.
Floats follow IEEE 754: every result rounds to the nearest representable value, ties to even, and a result with no number for it is an infinity or NaN - never an error. = is exact IEEE equality, so compare computed floats with approxEquals, or by how many representable values apart they are with ulpDistance. NaN equals nothing, an infinity only itself. The usual mathematical functions are methods, and constants are functions on the type: F64.pi(), F64.e(), F64.infinity().
The bitwise operators are words - band, bor, bxor, shl, shr - with ~ for not. They bind tighter than comparisons and looser than arithmetic, so x band 1 = 1 tests the lowest bit. Shifting by the type's width or more shifts every bit out. For sets of flags, Mask is a fixed-size set of bits. See Compile-time Code, whose CRC-32 table is built from them.
A float can't hold 0.1 exactly, so sums of prices drift. A Decimal is a whole number of hundredths, thousandths or whatever its places are, so 0.1 + 0.2 is exactly 0.3. Nothing is rounded unless you ask: + and - keep the larger side's places and * adds them up. / can't always be exact, so it keeps 16 places - divideTo and round take the places you want and a RoundingMode, half to even unless you say otherwise. It holds up to 38 digits. Whole cents in an integer are exact too - Invoice Totals works that way - but a Decimal keeps fractions of a cent, and prints itself with its places.
When a result may not fit even an S128 or a U256 - factorials, combinatorics, exact powers - a BigInteger grows as it needs, so +, -, * and ^ never overflow. Division rounds the way the fixed integers' does. It's slower than a fixed integer and has no bit operations, so it's for the numbers that need it: make one with BigInteger.of, BigInteger.parse or an integer's toBigInteger(), and come back with toInteger<U64>(), an OverflowError when it doesn't fit.
A Rational is an exact fraction - a numerator over a denominator, both BigIntegers, kept in lowest terms - so a third stays a third and Rational.of(2, 4) = Rational.of(1, 2). Where a Decimal is exact in tenths and hundredths, a Rational is exact in any fraction; floor, round, toDecimal and toF64 leave it when you want a rounded answer.
A Quantity<F64, Meter> is a number in meters. The language's units each live in #builtin.unit, grouped by what they measure - #builtin.unit.length.Meter, #builtin.unit.time.Second - and a type alias brings one out under its short name. The unit lives only in the type, so it costs nothing at run time, and the checker keeps units apart:
+, - and comparisons work within one unit, and * and / scale by a plain number. Between two quantities, * and / (or times and per) make the unit that combines them: distance / time is in Per<Meter, Second>, force * distance in Product<Newton, Meter>, and a unit times itself is written as its power - Meter².
The language knows the units people measure in - the SI ones (Meter, Kilogram, Second, Ampere, Kelvin, Mole, Candela) and what's made of them (Newton, Joule, Watt, Pascal, Volt, Hertz, ...), prefixed ones (Kilometer, Milligram, Kilowatt), everyday ones (Minute, Hour, Liter, KilometerPerHour, Celsius, Byte, Gibibyte), and imperial and US ones (Inch, Foot, Mile, Pound, Gallon, Fahrenheit) - in groups by what they measure (length, mass, time, temperature, area, volume, speed, energy, power, pressure, information, ...), the full list with Quantity. Units are compared by what they come to, so Product<Meter, Meter> is SquareMeter and Per<SquareMeter, Meter> is Meter. as and convert change a quantity into another unit of what it measures, and units that cancel leave a plain number:
A Kilometer isn't a Meter - adding the two is an error until one is converted - and converting a Meter into a Second is an error too. A converted amount keeps 12 significant digits, past which is only the rounding of the units' scales, and an integer quantity's amount is rounded to the nearest.
Any empty struct is a unit - one with no @unit a dimension of its own, one with @unit(...) made of other units: a number times units multiplied, divided and raised to whole powers. offset says where its zero is, for temperatures - @unit(Kelvin, offset ≔ 273.15) is how Celsius is made.
convert<Target>(factor), with a factor, changes a quantity into any unit by the factor you give - steps.convert<Meter>(0.75) - for the units that have no fixed one.
Complex, Quaternion, Vector<T, size> and Matrix<T, rows, cols> are builtin, with their arithmetic working through the usual operators. A vector's size and a matrix's dimensions are part of its type, so shapes that don't fit together are a type error rather than a surprise at run time. See Fractals.