Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Numbers reference

The sized numeric family (std::number), generic min/max (std::math), and random values (std::random). Literal syntax and conversion semantics: Values and types.

The family

TypeWidthLiteral
i8 i16 i32 i53signedbare = i32; others suffixed (100i53)
u8 u16 u32 u53unsignedsuffixed (0xFFu8)
usizeunsigned — the index type (below)from context, or 42usize
f64float2.5 or 10f
f32float2.5f32
BigIntarbitrary (i128 on the native backend — see the native guide)7n

i53/u53 are the wide integers, named for the precision they actually deliver: they are f64-backed on the JS backend, and every value in ±2^53 (f64’s exact-integer window, one past JavaScript’s Number.MAX_SAFE_INTEGER) is exact. There is no i64: a type that silently loses precision past 2^53 would be lying about its width; for bigger integers use BigInt.

Literals are range-checked at compile time (an out-of-range i53 literal is a compile error, not a rounded value). Integer division truncates toward zero. No implicit width coercion; convert with as_*. Arithmetic that overflows a type’s range is undefined behavior (spec §7.2a): on JS it manifests as f64 artifacts; a checked add_safe family is recorded future work.

Because overflow is undefined, the boundary values have to be askable. Every integer type carries its two bounds as niladic functions:

fun main() {
	print(i32::max_value());   // 2147483647
	print(i32::min_value());   // -2147483648
	print(u8::max_value());    // 255
	print(i53::min_value());   // -9007199254740992
}
typemin_value()max_value()
i8-128127
u80255
i16-3276832767
u16065535
i32-21474836482147483647
u3204294967295
i53-90071992547409929007199254740992
u5309007199254740992
usize09007199254740992

This spelling is a stopgap. vilan has no associated constants — there is no static-member mechanism for i32::MAX to hang on — so the bounds ship as functions rather than wait for that design. When it lands they become i32::MAX/i32::MIN and this pair enters a [deprecated("steer")] window that rewrites callers. The rename is scheduled, not a surprise: reach for max_value()/min_value() freely today.

The pair reports the type’s range, and a literal is checked against the same range: 127i8 and -128i8 compile, and 128i8 is refused as out of range for i8 (the minimum’s magnitude is admitted only under its -).

Floats have no pair, for two reasons that are worth stating rather than guessing at. f64’s finite bounds cannot be written as vilan literals at all — there is no exponent syntax, so 1.7976931348623157e308 is a parse error — and min_value() would have to silently pick between the most-negative finite (Rust’s f64::MIN) and the smallest positive normal (C’s DBL_MIN). That is a choice the eventual f64::MIN should make deliberately, not one this stopgap should prejudge. BigInt has no bounds by construction.

Methods

Integers (per type; shown for i32):

impl i32 {
	fun abs(self): i32
	fun pow(self, exponent: i32): i32
	fun min(self, other: i32): i32
	fun max(self, other: i32): i32
	fun rem(self, m: i32): i32     // the % operator's method
	fun diff(self, other: i32): i32
	fun is_even(self): bool
	fun is_odd(self): bool
}

Floats add the usual math surface:

impl f64 {
	fun abs(self): f64
	fun sqrt(self): f64
	fun pow(self, exponent: f64): f64
	fun floor(self): f64
	fun ceil(self): f64
	fun round(self): f64
	fun min(self, other: f64): f64
	fun max(self, other: f64): f64
	fun clamp(self, min: f64, max: f64): f64
	fun trunc(self): f64
	fun fract(self): f64
	fun sign(self): f64
	fun lerp(self, to: f64, t: f64): f64
	fun sin(self): f64      // cos, tan, asin, acos, atan, atan2, hypot
	fun exp(self): f64      // ln, log2, log10, cbrt
	fun to_radians(self): f64  // to_degrees

	// the three a reader actually comes looking for
	fun is_nan(self): bool
	fun is_finite(self): bool
	fun is_infinite(self): bool
}

Every numeric type implements Default (zero), the operator traits, comparison, and Display (the same text interpolation prints).

usize: the index type

A position in a sequence, a length, a count, the answer of a search for a position — that is what usize is for. It is a distinct numeric type, not another name for u53: on the JS backend it is represented as u53 is, and natively it is the platform word (Rust’s usize), which is exactly what a native len() already answers.

fun main() {
	let letters = ["a", "b", "c", "d"];
	let at: usize = 2;                  // a literal takes `usize` from context
	print(letters[at]);                 // c
	let step = 3usize;                  // the suffix exists, and is rarely needed
	print(i"{at + step}");              // 5
	print(i"{usize::max_value()}");     // 9007199254740992, on every backend
	print(at.checked_sub(step).is_none());  // true
	print(i"{at.saturating_sub(step)}");    // 0
	print(i"{step.as_i32()} {7.as_usize()}");
}

Its range is the JavaScript guarantee on every backend: [0, 2^53], the same window as u53, and usize::max_value() answers 9007199254740992 natively too — a program that printed the platform’s real ceiling would print different things on the two backends. A program that wants the native word’s ceiling is asking a native question.

Subtracting past zero is unspecified, never memory-unsafe. No check is emitted for a - b with b > a. On the JS backend the value simply leaves the type’s range and goes negative (0usize - 1usize prints -1); natively Rust’s own rule applies — a panic in a debug build, a wrap in a release build. Neither can reach memory it should not: every subscript keeps its bounds check, and a negative index fails it (index out of bounds: the length is 2 but the index is -1). Because the two backends legitimately disagree, a program that underflows is outside what the native differential compares. When you want a defined answer, ask for one: checked_sub answers None past zero, and saturating_sub stops at 0. A downward loop is written so that it never reaches the edge — for i > 0 { i -= 1; … }, not for i >= 0 { …; i -= 1 }, whose guard an unsigned counter can never make false.

Converting goes through the family’s as_* methods in both directions: every numeric type has as_usize(), and usize has as_i8() … as_f64(). A mismatch names the conversion it wants, and names an index as one (Expected usize (an index: a position, a length or a count), but got i32 instead. There are no implicit numeric conversions; convert with `.as_usize()` ); the editor offers to write it, or to declare a literal-bound counter usize instead, and vilan check --fix writes every one of them in a package (see the CLI).

On the wire a usize travels at i32’s width, not u53’s — a length or a position that crosses rpc keeps the width it has always had, so frames do not grow when a position is respelled usize (see Encoding). A usize past i32::MAX is not describable on the wire, and nothing std builds can produce one.

What is not an index. An identity (a channel, a connection, a row id) is not a position, nor is an element’s VALUE (a byte, a character code), a duration or a port. A file offset is not an index either: it is a position in a stream that is not in memory, its range is the filesystem’s, and std::fs keeps it i53.

std speaks it everywhere. Every length, count and position in std is a usize: len() on a list, a string, a map, a set and Bytes; the index get(i), insert(i, ..), remove(i), substring(from, to) and code_at(i) take; index_of’s answer; take(n)/skip(n)/repeat(n); enumerate()’s counter ((usize, T)); and xs[i], whose index is a usize and nothing else. The values beside them keep their own types — a byte Bytes::get answers is still an i32, an id is still an id. A program written against the i32 spellings migrates with vilan check --fix (see the CLI); what it cannot decide is a -1 “not found” (an Option<usize> or a real fallback), a for i >= 0 loop (count down from len with for i > 0 { i -= 1; … }), and signed arithmetic that should stay signed and convert once.

clamp confines a value to a range. The integers inherit it from Ord; the floats are deliberately not Ord (NaN has no place in a total order), so f64 and f32 carry their own — same recipe, same result.

import std::compare::Ord;

fun main() {
	print(9.clamp(0, 5));       // 5   — i32, through Ord
	print(9f.clamp(0f, 5f));    // 5
	print((0f - 1f).clamp(0f, 5f));   // 0
}

Conversions: as_*

Every numeric type converts to every other with Rust-as semantics. Floats truncate toward zero; integers fold two’s-complement into the target width:

fun main() {
	print((3.9).as_i32());    // 3
	print((-1).as_u8());      // 255 — folded
	print((300).as_u8());     // 44
	let wide = 9007199254740992i53;
	print(wide.as_i32());
	print((255u8).as_f64() / 2.0);
}

The two unsigned widths that do not fold, u53 and usize, saturate instead: a negative converts to 0 on every backend, as a native f64 as u64 does, and a positive truncates as usual.

fun main() {
	print((-5).as_usize());   // 0 — saturated, not folded
	print((-2.5).as_u53());   // 0
	print((7.9).as_usize());  // 7
}

Conversions on literals fold at compile time.

std::math

fun min<T: Ord>(a: T, b: T): T
fun max<T: Ord>(a: T, b: T): T
fun minmax<T: Ord>(a: T, b: T): (T, T)   // (smaller, larger)

struct Vec2 { x: f64, y: f64 }           // a 2-D vector: point, offset, velocity, size
impl Vec2 {
	fun scale(self, factor: f64): Vec2   // both components times a scalar
	fun length(self): f64                // sqrt(x² + y²)
	fun length_squared(self): f64        // x² + y², for comparing without the root
	fun distance(self, other: Vec2): f64 // the length of the vector between two points
	fun dot(self, other: Vec2): f64      // x*x + y*y; v.dot(v) is v's length squared
}
// `Add`, `Sub` (component-wise, so `a + b` and `a - b`) and `PartialEq`.

Vec2 is a value, not a handle: it copies, compares and parks in a signal. The components carry no unit — CSS pixels at a UI call site, metres at a physics one — which is why there is no Length type in the signature.

import std::math::Vec2;

fun main() {
	let start = Vec2 { x = 10.0, y = 10.0 };
	let now = Vec2 { x = 13.0, y = 14.0 };
	let travelled = now - start;
	print(travelled.length());              // 5
	print(now.distance(start) > 3.0);       // a drag threshold
}

A threshold does not need the square root: travelled.length_squared() is x² + y², and sqrt is monotonic, so compare it against the squared threshold on a hot path — a pointer-move handler squares its threshold once instead of taking a root per event. Take the root when the number itself is the answer. == is exact f64 equality, with everything that implies — a vector arrived at by arithmetic is rarely == one written down, so compare a distance against a tolerance where that matters.

std::random

fun range<T: Random>(low: T, high: T): T   // uniform in [low, high)
// implemented for i32, u32, f64

The range is half-open: low is reachable, high is not, for the integer arms and the float one alike. That is what makes range(0, items.len()) an index, and range(4, 4) answers 4 — the empty range has nothing else to answer.

import std::io::panic;
import std::random;

fun main() {
	let roll = random::range(1, 7);   // 1..=6, never 7
	if roll < 1 || roll > 6 {
		panic(i"a d6 rolled {roll}");
	}
	print("rolled");
}

The fence panics rather than printing a bool, deliberately: a bool tells a reader what the range is and tells the gate nothing, and this one was printing false about one run in eight while the line above it said 1..=6 (A118).

Not cryptographic: for tokens and ids use std::crypto (random_uuid, random_bytes; see misc).