rokdocs
v0.1

2.1. Numeric types

The rok::core::num module defines every numeric type of rok. rok keeps numbers from two worlds side by side, and the user is free to choose either.

int / float    (auto-promote, arbitrary precision, opt-in)
fixed primitive (i8..i64, u8..u64, isize, usize, f32, f64; hardware-friendly, default)

The two worlds coexist. An unsuffixed literal follows the expected type; if there is no expected type it is i32 / f64 (refs-lang 1.6.3). int / float are used only where the type is written out.

2.1.1. Two worlds of numbers - why they are separate

int / float - the auto-promote world

This world aims at arbitrary precision. Overflow never happens; computation ends with OOM only when the memory limit is reached. Small values go through the small-int optimization and are stored in hardware fixed-width form; as a value grows it is automatically promoted to a heap BigInt. int / float values are immutable, and a heap BigInt is never modified in place, only shared (which is why they are Copy). The user is thereby guaranteed arithmetic correctness: I32_MAX + 1 never silently wraps into an absurd result.

Fixed primitives - the hardware-friendly world

Fixed primitives, by contrast, are the world close to the hardware. They comprise 15 types such as i32 / f64; their syntax is defined by refs-lang 2.1 Type expressions. They suit situations that need precise control of memory and performance (FFI interfaces, SIMD, large arrays, hot loops), and their overflow policy is explicit (see the fixed-width arithmetic section of 2.3 ops arithmetic).

Which one is the default

An unsuffixed literal (42, 3.14) is i32 / f64 when there is no expected type. The auto-promote world is used only where int / float is written out.

let total: int = 0;               // int (auto-promote) - explicit
for x in xs { total = total + x; }
// total is auto-promoted even past the i32 range - no silent overflow

let n = 0;                        // i32 (no expected type)
let perf_critical: i64 = 0;       // i64 - follows the expected type

2.1.2. int - arbitrary-precision integer

#[keyword("int")]
#[lang("int_type")]
struct Int { ... }  // internal representation: small-int or BigInt

impl Int {
    fn new() -> int;              // initialized to 0
    fn from_i64(n: i64) -> int;
    fn from_u64(n: u64) -> int;
    fn parse_radix(s: str, radix: u32) -> int | none;  // radix parsing (base 10 is FromStr)

    fn to_i32(self) -> i32 | none;  // the value if in range, none otherwise
    fn to_i64(self) -> i64 | none;
    fn to_f64(self) -> f64;        // always succeeds (may lose precision)

    fn pow(self, exp: u32) -> int;              // exp: u32 as for fixed-width integers
    fn isqrt(self) -> int;                      // floor(sqrt(self)); panics if self < 0
    fn checked_isqrt(self) -> int | none;       // none if self < 0
    fn div_euclid(self, rhs: int) -> int;       // the quotient for which rem_euclid >= 0; panics if rhs == 0
    fn rem_euclid(self, rhs: int) -> int;       // in 0..rhs.abs(); panics if rhs == 0
    fn modpow(self, exp: int, m: int) -> int;   // self^exp %% m; panics if m == 0 or exp < 0
    fn gcd(self, other: int) -> int;            // never negative; gcd(0, 0) == 0
    fn lcm(self, other: int) -> int;            // never negative; 0 if either operand is 0
    ...
}

These follow the fixed-width integers' names and panics. pow takes a u32 exponent: a larger one gives at least 2^32 bits for any base but 0 and ±1. modpow takes an int exponent, since its result stays below |m|; the result has the sign of m, like %%. A negative exponent is an error (no modular inverse).

Int is defined in rok::core::num::bigint::Int (Float in rok::core::num::bigfloat::Float). #[keyword("int")] registers the identifier int as a type keyword, and this registration applies to every package. User code therefore writes the type as lowercase int, not PascalCase Int (refs-lang 6.6 is authoritative). The compiler finds the int type through #[lang("int_type")].

The key properties of int are as follows. In memory, small values go through the small-int optimization and are stored directly in a 64-bit word, while large values are automatically promoted to a heap BigInt representation; all of this is transparent to the user. The representation is always canonical: a value within the i64 range is in small form, otherwise it is a BigInt, and when an arithmetic result returns to the i64 range it returns to small form. Equal values have equal representations. The lang slot used is int_type (0.4 lang slot table). An unsuffixed integer literal is int only when the expected type is int.

Arithmetic

Arithmetic between ints never wraps or saturates.

let big: int = 1;
big = big * 1000;
big = big * 1000;
big = big * 1000;
big = big * 1000;
big = big * 1000;
big = big * 1000;
// big = 10^18 - near the end of the i64 range
big = big * 1000;
// big = 10^21 - outside the i64 range, promoted to BigInt
big = big * big;
// big = 10^42 - a larger BigInt, exact

All of the arithmetic traits (2.3 ops) Add, Sub, Mul, Div, Rem are implemented. Div is integer division (truncating), so 7 / 2 = 3 and -7 / 2 = -3. The comparison traits (2.5 cmp) Eq, Ord, Hash are all implemented as well.

Main uses

int serves in many situations. As the arithmetic default in a REPL or Jupyter, it guarantees correctness without typing annotations every time; accumulated sums or products in numeric simulations carry no overflow concern. Cryptographic operations and large-integer processing work with the standard library alone, without depending on a separate BigInt library.

Limitations

int has limitations. In memory, BigInt promotion requires heap allocation, so fixed primitives are recommended in hot loops and large arrays. In performance, it is close to fixed primitives within the small-int range, but necessarily slower after BigInt promotion. For FFI, extern "C" functions accept only fixed primitives, so an int must be explicitly converted to i64 or similar before being passed.

2.1.3. float - floating-point with unbounded exponent

#[keyword("float")]
#[lang("float_type")]
struct Float { ... }  // internal: f64 or BigFloat (when the exponent is outside f64)

impl Float {
    fn from_f64(x: f64) -> float;
    fn try_parse(s: str) -> float | none;          // FromStr is implemented too

    fn to_f32(self) -> f32 | none;     // none only when a finite value is outside the f32 range
    fn to_f64(self) -> f64;
    ...
}

#[keyword("float")] registers the float keyword and routes it to struct Float. User code therefore sees only lowercase float.

A float value has a 53-bit significand (the same precision as f64) and an unbounded (i64) exponent. It normally uses the f64 representation; when a finite result exceeds the f64 range (overflow) or becomes too small toward 0 (underflow), it is promoted to a BigFloat representation instead of becoming inf / 0.

  • inf / NaN arise only from division by zero or from inf / NaN operands.
  • % is an exact fmod.
  • Display has the same shape as f64's %g; a promoted value is written with an exponent, as in 1.5e+400.

Stated limitations

The limitations of float must be stated clearly. Its precision is 53 bits, not unbounded, so it has the same round-off as IEEE 754 f64. For example, 0.1 + 0.2 not being 0.3 happens in rok as well. Eq is also not implemented: float does not implement the Eq marker trait and implements only PartialEq, to handle NaN. See 2.5 cmp for the full hierarchy.

lang slot

The compiler finds the float type through float_type (0.4 lang slot table). An unsuffixed floating-point literal is float only when the expected type is float.

2.1.4. Fixed-width primitives

refs-lang 2.1 Type expressions defines the names of the 15 primitives. This section defines the standard library methods and behavior on top of that.

Integers (10 types)

TypeBit widthRange
i88-128 ~ 127
i1616-32768 ~ 32767
i3232-2³¹ ~ 2³¹-1
i6464-2⁶³ ~ 2⁶³-1
isizetarget pointer width (32 or 64)target-dep
u880 ~ 255
u16160 ~ 65535
u32320 ~ 2³²-1
u64640 ~ 2⁶⁴-1
usizetarget pointer widthtarget-dep

Floating-point (2 types)

TypeBit widthStandard
f3232IEEE 754 binary32 (single precision)
f6464IEEE 754 binary64 (double precision)

Boolean / character (3 types)

TypeMeaning
booltrue / false
charUnicode scalar value (4 bytes), compiler primitive. Methods in 2.15
c_charC ABI compatible char (1-byte signed); FFI only

Two groups - language built-ins vs standard library additions

The numeric / boolean / character types of this chapter fall into two groups.

GroupMembersRegistration
16 language built-insi8/i16/i32/i64, u8/u16/u32/u64, isize/usize, f32/f64, bool, char, c_char, strRecognized by the language - no #[keyword] needed
2 standard library additionsint, floatRegistered with #[keyword(...)]

Language built-ins are tokens the compiler (lexer/parser) recognizes directly, so the standard library does not register them as keywords. This automatically guarantees that users cannot redefine these names.

The standard library additions (int / float), in contrast, are registered with #[keyword(...)]. The #[keyword] attribute is attached to the Int / Float definitions in this module (refs-lang 6.6 is authoritative).

Standard library form of the built-in primitives

struct i32 { ... }        // used directly by users (the lowercase identifier is the type)
struct f64 { ... }
struct bool { ... }
struct c_char { ... }
struct str { ... }
// ... the remaining 11 have the same shape

The standard library only attaches impl blocks to implement traits (Add, Display, Clone, ...). Recognizing the type itself is the compiler's responsibility.

Arithmetic / comparison trait impls

Each primitive implements the traits of 2.3 ops, 2.4 ops-assign and 2.5 cmp. Every impl carries #[intrinsic("rok.OP.TYPE")], which binds it directly to a compiler backend PrimOp.

Methods (per primitive)

Each integer / floating-point type exposes the following methods (the example is i32).

impl i32 {
    const MIN: i32 = -2147483648;
    const MAX: i32 = 2147483647;
    fn abs(self) -> i32;
    fn pow(self, exp: u32) -> i32;
    fn count_ones(self) -> u32;
    fn leading_zeros(self) -> u32;
    fn trailing_zeros(self) -> u32;
    fn rotate_left(self, n: u32) -> i32;
    fn rotate_right(self, n: u32) -> i32;
    fn swap_bytes(self) -> i32;
    fn to_le_bytes(self) -> [u8; 4];
    fn to_be_bytes(self) -> [u8; 4];
    fn from_le_bytes(bytes: [u8; 4]) -> i32;
    fn from_be_bytes(bytes: [u8; 4]) -> i32;

    // explicit-overflow variants
    fn checked_add(self, rhs: i32) -> i32 | none;
    fn checked_sub(self, rhs: i32) -> i32 | none;
    fn checked_mul(self, rhs: i32) -> i32 | none;
    fn checked_div(self, rhs: i32) -> i32 | none;
    fn checked_rem(self, rhs: i32) -> i32 | none;
    fn checked_neg(self) -> i32 | none;
    fn checked_shl(self, n: u32) -> i32 | none;
    fn checked_shr(self, n: u32) -> i32 | none;

    fn saturating_add(self, rhs: i32) -> i32;     // clamp to i32::MAX or MIN
    fn saturating_sub(self, rhs: i32) -> i32;
    fn saturating_mul(self, rhs: i32) -> i32;

    fn wrapping_add(self, rhs: i32) -> i32;       // modular arithmetic
    fn wrapping_sub(self, rhs: i32) -> i32;
    fn wrapping_mul(self, rhs: i32) -> i32;
    fn wrapping_neg(self) -> i32;
    fn wrapping_shl(self, n: u32) -> i32;
    fn wrapping_shr(self, n: u32) -> i32;
}

Unsigned, floating-point and isize types follow a similar pattern, each exposing only the methods appropriate to it.

The minimum / maximum of an integer type are the associated constants i32::MIN / i32::MAX, not functions. An argument-less inherent method min() / max() would shadow Ord::min / Ord::max (a.min(b)) of the same name.

Floating-point types (f32, f64) provide these additional methods.

impl f64 {
    fn nan() -> f64;             // f64::NAN
    fn infinity() -> f64;        // f64::INFINITY
    fn neg_infinity() -> f64;
    fn epsilon() -> f64;         // 2^-52

    fn is_nan(self) -> bool;
    fn is_infinite(self) -> bool;
    fn is_finite(self) -> bool;
    fn is_sign_positive(self) -> bool;
    fn is_sign_negative(self) -> bool;

    fn floor(self) -> f64;
    fn ceil(self) -> f64;
    fn round(self) -> f64;
    fn trunc(self) -> f64;
    fn fract(self) -> f64;       // self - self.trunc()

    fn abs(self) -> f64;
    fn signum(self) -> f64;      // 1.0 if positive, -1.0 if negative, that zero if ±0.0, NaN if NaN

    fn sqrt(self) -> f64;
    fn cbrt(self) -> f64;
    fn pow(self, exp: f64) -> f64;
    fn exp(self) -> f64;
    fn ln(self) -> f64;
    fn log(self, base: f64) -> f64;
    fn log2(self) -> f64;
    fn log10(self) -> f64;
    fn sin(self) -> f64;
    fn cos(self) -> f64;
    fn tan(self) -> f64;
    fn asin(self) -> f64;
    fn acos(self) -> f64;
    fn atan(self) -> f64;
    fn atan2(self, other: f64) -> f64;
    fn sinh(self) -> f64;
    fn cosh(self) -> f64;
    fn tanh(self) -> f64;

    fn to_bits(self) -> u64;     // bit pattern
    fn from_bits(bits: u64) -> f64;
}

2.1.5. Literal → which type?

The rules of refs-lang 1.6 Numeric literals are:

FormResulting type
42Expected type (integer / floating-point type), else i32
3.14Expected type (floating-point type), else f64
42i32i32
42u64u64
3.14f32f32
3.14f64f64
'A'char
c'A'c_char (primitive)
0xFFSame as decimal - expected type, else i32
0xFFi32i32

Expected type and range checking

Ranges are checked at compile time.

let x: i32 = 42;            // i32
let y: i32 = 9999999999;    // compile error (outside the i32 range)
let z: f32 = 3.14;          // f32
let f: f64 = 1;             // f64 - an integer literal fits a floating-point slot
let w: u8 = 256;            // compile error (256 ∉ u8 range 0..=255)
let i: i32 = 1.5;           // compile error - a floating-point literal cannot fill an integer slot
let big: int = 9999999999;  // int

The full conversion matrix is in 2.11 Conversion.

Zero / One

trait Zero { fn zero() -> Self; }       // additive identity
trait One  { fn one() -> Self; }        // multiplicative identity

Every numeric primitive and int / float implement both traits. Iterator::sum / product use them (2.7.1).

2.1.6. Arithmetic result types

int + int = int

Because auto-promote applies, the result is always int itself. No fixed-width inference takes place.

let a: int = 1000;
let b: int = 2000;
let c = a + b;       // int (3000)

i32 + i32 = i32

Fixed width is preserved. A user who wants a wider type must use an explicit conversion.

let a: i32 = 5;
let b: i32 = 3;
let c: i32 = a + b;   // i32

let wider: i64 = (a as i64) + (b as i64);   // i64

int + i32 (mixed - rejected)

let a: int = 5;       // int
let b: i32 = 3;       // i32
let c = a + b;        // compile error - type mismatch

An explicit conversion is required here.

let c1 = a + int::from_i32(b);    // int (a + int(3))
let c2 = (a.to_i32()? + b);       // i32 (OK if a is within the i32 range)

This policy blocks silent type mixing. Unlike Julia, rok does not auto-promote; the user states which world to use.

2.1.7. Fixed primitive ↔ int conversion

Conversion between fixed primitives and int is explicit only.

ConversionMethodOn failure
i32 → int.into::<int>() or int::from_i32Always succeeds (lossless)
int → i32.try_into::<i32>()Throws OverflowError
int → i32 (saturate).saturating_into::<i32>()i32::MIN or MAX
int → i32 (wrap).wrapping_into::<i32>()Modular arithmetic
int → f64.into::<f64>() or int::to_f64Rounds to the nearest value (a documented exception to From, 2.11.8). float → f64 is the same exception

2.1.8. Niche optimization (brief)

refs-lang 2.7 niche defines the mechanism. The primitives that get an automatic niche are:

bool | none is 1 byte (niche value 2), and Ptr<T> | none / ConstPtr<T> | none are pointer-sized (null is none) (refs-lang 2.7.2). i8..i64, u8..u64, f32 and f64 themselves get no automatic niche, because every bit pattern is a valid value.

When an opt-in niche is needed, use the NonZero<T> / NonNan<F> / Finite<F> / Positive<T> wrappers of 2.2 non-zero.

2.1.9. lang slot mapping

The 16 language built-ins use lowercase struct names. The compiler recognizes them directly, so no #[keyword] is needed.

primitivelang slotstruct nameRegistration
i8i8_typei8Language built-in
i16i16_typei16Language built-in
i32i32_typei32Language built-in
i64i64_typei64Language built-in
u8u8_typeu8Language built-in
u16u16_typeu16Language built-in
u32u32_typeu32Language built-in
u64u64_typeu64Language built-in
isizeisize_typeisizeLanguage built-in
usizeusize_typeusizeLanguage built-in
f32f32_typef32Language built-in
f64f64_typef64Language built-in
boolbool_typeboolLanguage built-in
c_charcchar_typec_charLanguage built-in
charchar_typecharLanguage built-in
strstr_typestrLanguage built-in

The 3 types added by the standard library are registered as PascalCase structs with #[keyword(...)].

primitivelang slotstruct nameRegistration
intint_typeInt#[keyword("int")]
floatfloat_typeFloat#[keyword("float")]

Range<T> (a..b) is defined by 2.7 iter.

2.1.10. Comparison with other languages - design rationale

rok's two-world model (auto-promote int and fixed primitives) draws on several languages. Smalltalk's SmallInteger ↔ LargeInteger is the prototype of auto-promote, and the numeric tower of Lisp and Scheme (fixnum / bignum / rational / real) is another important reference. Python int makes arbitrary precision the default but has no fixed primitives, while Julia makes fixed primitives the default. In rok too, literals default to fixed (i32 / f64), and int is opt-in.

The differences from each language: Python offers only int; lacking fixed width, it depends on NumPy for FFI and performance-critical work. Julia, like rok, defaults to fixed and requires an explicit BigInt when arbitrary precision is needed. Rust is fixed only, so the user must always write the type. rok lets the two worlds coexist: literals follow the expected type (i32 / f64 if none), and int / float are written where arbitrary precision is needed.

Edit this page on GitHub