rokdocs
v0.1

2.3. Arithmetic / bitwise / indexing traits

The rok::core::ops module defines the trait family to which rok's operator tokens (refs-lang 1.10) dispatch. Not only the ordinary operators + - * / % << >> & | ^ ~ && || but also arr[i] indexing and s[1:3] slicing call trait methods defined in this chapter.

The mechanism of refs-lang 3.1 Operator dispatch looks up the contents of this chapter; this document, refs-standard-libraries 2.3, is the authority for them.

Note

Every operator trait method in this chapter is declared without throws. Operators therefore do not throw errors, and impls cannot add throws (refs-lang 4.14.11). A fallible operation is provided as a named method (checked_div and the like) or returns a Result.

2.3.1. Arithmetic traits

Add - arithmetic addition (+)

The first is the addition trait.

#[lang("add_trait")]
trait Add<Rhs = Self> {
    type Output;

    #[infix("+")]
    fn add(self, rhs: Rhs) -> Output;
}

Meaning: returns the sum of two values.

let a: i32 = 5;
let b: i32 = 3;
let c = a + b;        //→ 8 (i32)

let x: f64 = 1.5;
let y: f64 = 2.5;
let z = x + y;        //→ 4.0 (f64)

Output rules

The Output associated type is usually the same as Self, with these exceptions. First, for int + int = int, auto-promote applies: if the result of i32 + i32 lies in the i64 range it is promoted automatically. Second, user-defined arithmetic such as Vec3<f64> + f64 = Vec3<f64> is defined in the form impl Add<f64> for Vec3<f64>.

Overflow behavior

Overflow behavior depends on the type. For i32 + i32, overflow panics when overflow checks are on and wraps when they are off. Checks are controlled by rokc -C overflow-checks[=yes|no] (refs-lang 9.1.2); by default they are on only at -O0. f32 + f32 and f64 + f64 follow IEEE 754 addition, so overflow results in ±∞ or NaN. int + int has arbitrary precision, so overflow does not exist. Saturating, checked and wrapping variants are provided as separate methods (saturating_add, checked_add, wrapping_add) defined outside this trait.

For the full integer overflow policy, see the fixed-width arithmetic section of 2.11 Conversion.

Sub - arithmetic subtraction (-)

The subtraction trait is defined as follows.

#[lang("sub_trait")]
trait Sub<Rhs = Self> {
    type Output;

    #[infix("-")]
    fn sub(self, rhs: Rhs) -> Output;
}

It has the same form as Add; a - b dispatches to Sub::sub(a, b).

let r = 10i32 - 3i32;     //→ 7
let s = 1.0f64 - 0.5f64;  //→ 0.5

Mul - arithmetic multiplication (*)

The multiplication trait is defined as follows.

#[lang("mul_trait")]
trait Mul<Rhs = Self> {
    type Output;

    #[infix("*")]
    fn mul(self, rhs: Rhs) -> Output;
}
let r = 6i32 * 7i32;      //→ 42
let s = 2.5f64 * 4.0f64;  //→ 10.0

User-defined meaning

Besides arithmetic multiplication, Mul may carry a domain-specific meaning, matrix multiplication being the typical example.

struct Matrix2x2 { a: f64; b: f64; c: f64; d: f64; }

impl Mul<Matrix2x2> for Matrix2x2 {
    type Output = Matrix2x2;

    fn mul(self, rhs: Matrix2x2) -> Matrix2x2 {
        Matrix2x2 {
            a: self.a * rhs.a + self.b * rhs.c,
            b: self.a * rhs.b + self.b * rhs.d,
            c: self.c * rhs.a + self.d * rhs.c,
            d: self.c * rhs.b + self.d * rhs.d,
        }
    }
}

Element-wise multiplication is covered by DotMul (m .* n) in 2.6 broadcast.

Div - arithmetic division (/)

The division trait is defined as follows.

#[lang("div_trait")]
trait Div<Rhs = Self> {
    type Output;

    #[infix("/")]
    #[infix("÷")]
    fn div(self, rhs: Rhs) -> Output;
}
let r = 20i32 / 7i32;      //→ 2 (integer division, truncating)
let s = 20.0f64 / 7.0f64;  //→ 2.857142857142857

Division by zero

Division by zero is handled per type. Integer i32 / 0 panics, and an expression that constant-folds (1 / 0) is a compile error. Floating-point f64 / 0.0 yields ±∞ or NaN per IEEE 754.

To get none instead of a panic, use checked_div.

let r = 20i32.checked_div(7i32);  //→ 2
let s = 20i32.checked_div(0i32);  //→ none

Rem - arithmetic remainder (%)

The remainder trait is defined as follows.

#[lang("rem_trait")]
trait Rem<Rhs = Self> {
    type Output;

    #[infix("%")]
    fn rem(self, rhs: Rhs) -> Output;
}
let r = 20i32 % 7i32;     //→ 6
let s = 5.5f64 % 2.0f64;  //→ 1.5 (IEEE 754 fmod)

Sign rule: the sign of the result follows the dividend (left operand), so (-5) % 3 = -2 and 5 % (-3) = 2 (the truncated-division definition, as in C / Rust).

Modulo - floored modulo (%%)

The floored (quotient rounded down) remainder trait is defined as follows.

trait Modulo<Rhs = Self> {
    type Output;

    #[infix("%%")]
    fn modulo(self, rhs: Rhs) -> Output;
}
let r = (-5i32) %% 3i32;     //→ 1   ( = -5 - (-2)*3 )
let s = 5i32 %% (-3i32);     //→ -1  ( = 5 - (-2)*(-3) )
let t = (-7.5f64) %% 2.0f64; //→ 0.5

It differs from Rem in the sign rule of the result. a %% b = a - floor(a / b) * b, so the sign of the result follows the divisor b (like Python's %). For a positive divisor the result always lies in [0, b), giving a positive residue even for a negative dividend; for a negative divisor it lies in (b, 0] (7 %% -3 = -2). It differs from Euclidean modulo (always non-negative) for negative divisors. It is common in index wrap-around and modular arithmetic. A zero result of a float %% has the divisor's sign (44.0 %% -4.0 = -0.0).

// common pattern - negative index wrap-around
let i: i32 = -1;
let len: i32 = 8;
let wrapped = i %% len;      //→ 7    (i % len would give -1)

The sign difference between Rem and Modulo is summarized below.

aba % b (Rem)a %% b (Modulo)
7311
-73-12
7-31-2
-7-3-1-1

The two tokens map to separate traits, so a type may implement only one of them or give them different meanings. The standard library's primitive integer / floating-point impls follow the definitions in the table above exactly.

The %% token is defined in refs-lang 1.10 Punctuation and operator tokens; its precedence in the refs-lang 3.2 table is level 8, the same as %, left-associative.

2.3.2. Unary traits

Neg - unary negation (prefix -)

The unary negation trait is defined as follows.

#[lang("neg_trait")]
trait Neg {
    type Output;

    #[prefix("-")]
    fn neg(self) -> Output;
}
let r = -5i32;       //→ -5
let s = -3.14f64;    //→ -3.14
let t = -(a + b);    //→ -(a+b)

This trait is meaningful only for signed integers and floating-point types; u32 has no Neg impl.

In particular, the negation of i32::MIN (= -2147483648) is not representable, so it panics in debug builds and wraps in release builds (= i32::MIN unchanged). Use wrapping_neg for an explicit wrap, or checked_neg for a T | none result.

Not - bitwise complement / logical negation (!)

The Not trait is defined as follows.

#[lang("not_trait")]
trait Not {
    type Output;

    #[prefix("!")]
    fn not(self) -> Output;
}
let r = !true;       //→ false
let s = !0u8;        //→ 255 (all bits 1)
let t = !0xFFi32;    //→ 0xFFFFFF00 (all 32 bits inverted)

Per-type behavior: bool::not(true) = false and bool::not(false) = true, acting as logical negation. u8::not(0) = 0xFF and i32::not(0) = -1, by contrast, act as bitwise complement.

BitNot - bitwise complement (~)

The ~ token (refs-lang 1.10.3) is an integer-only bitwise complement.

#[lang("bitnot_trait")]
trait BitNot {
    type Output;

    #[prefix("~")]
    fn bitnot(self) -> Output;
}
let s = ~0u8;        //→ 255
let t = ~0i32;       //→ -1

For integer types, ~x gives the same result as !x. BitNot, however, is implemented only for i* / u* and not for bool, so ~true is a compile error. The recommended idiom is ! for logical negation and ~ when the intent is to show a bitwise complement.

2.3.3. Bitwise traits

BitAnd - bitwise AND (&)

The bitwise AND trait is defined as follows.

#[lang("bitand_trait")]
trait BitAnd<Rhs = Self> {
    type Output;

    #[infix("&")]
    fn bitand(self, rhs: Rhs) -> Output;
}
let r = 0xF0i32 & 0x0Fi32;   //→ 0x00
let s = 0xFFi32 & 0x0Fi32;   //→ 0x0F
let b = true & false;         //→ false (bool also has a BitAnd impl)

Note

&& and & differ. && is short-circuit logical AND; & is bitwise AND. For bool the two give the same result but evaluate at different times.

BitOr - bitwise OR (|)

The bitwise OR trait is defined as follows.

#[lang("bitor_trait")]
trait BitOr<Rhs = Self> {
    type Output;

    #[infix("|")]
    fn bitor(self, rhs: Rhs) -> Output;
}
let r = 0xF0i32 | 0x0Fi32;   //→ 0xFF

BitXor - bitwise XOR (^)

The bitwise XOR trait is defined as follows.

#[lang("bitxor_trait")]
trait BitXor<Rhs = Self> {
    type Output;

    #[infix("^")]
    fn bitxor(self, rhs: Rhs) -> Output;
}
let r = 0xFFi32 ^ 0x0Fi32;   //→ 0xF0
let s = 0b1010 ^ 0b1100;     //→ 0b0110

Shl - left bit shift (<<)

The left shift trait is defined as follows.

#[lang("shl_trait")]
trait Shl<Rhs = Self> {
    type Output;

    #[infix("<<")]
    fn shl(self, rhs: Rhs) -> Output;
}
let r = 1i32 << 4;     //→ 16
let s = 0xFFi32 << 24; //→ 0xFF000000

When the shift count is greater than or equal to the bit width, debug builds panic and release builds wrap (in practice this is close to undefined under LLVM's definition, so users are advised to use explicit wrapping).

let r = 1i32.wrapping_shl(40);  // explicit - shifts by 40 % 32 = 8

Shr - right bit shift (>>)

The right shift trait is defined as follows.

#[lang("shr_trait")]
trait Shr<Rhs = Self> {
    type Output;

    #[infix(">>")]
    fn shr(self, rhs: Rhs) -> Output;
}
let r = 256i32 >> 4;  //→ 16
let s = -1i32 >> 1;   //→ -1 (signed = arithmetic shift, sign extension)
let t = -1u32 >> 1;   //→ 0x7FFFFFFF (unsigned = logical shift)

Signed integers use an arithmetic shift (sign-extend); unsigned integers use a logical shift.

2.3.4. Indexing traits

Indexing and slicing are language syntax (refs-lang 3.6), but the actual dispatch goes through the traits of this section.

Index - single-element read (arr[i])

The Index trait is defined as follows.

#[lang("index_trait")]
trait Index<Idx> {
    type Output;

    fn index(self, idx: Idx) -> Output;
}
let xs: List<i32> = [10, 20, 30];
let v = xs[1];        //→ 20
let s: str = "hello";
let c = s.chars().next();   //→ 'h' (char | none) - str has no Index (2.9.8)

Bounds checking

Bounds are handled as follows. An out-of-range index (xs[100]) panics (operators do not throw errors). A negative signed-integer index counts from the end (xs[-1] is the last element, refs-lang 3.6.1). If len + i < 0, it panics.

To get a T | none result instead of a panic, use xs.get(i).

let v = xs.get(100);  //→ none

IndexSet - single-element write (arr[i] = v)

The IndexSet trait is defined as follows.

#[lang("index_set_trait")]
trait IndexSet<Idx> {
    type Value;

    fn index_set(self, idx: Idx, value: Value);     // collections are reference handles - self
}
let xs: List<i32> = [10, 20, 30];
xs[1] = 99;           // IndexSet::index_set(xs, 1, 99)
xs[1];                //→ 99

It is separate from Index because read-only containers (e.g. an immutable view of a tuple) must still be able to implement Index. IndexSet is therefore a separate opt-in.

IndexSet also panics when out of range, while xs.set(i, v) throws IndexError (throws (IndexError)).

Sliceable - sub-range extraction (s[a:b], s[a:b:step])

The slice trait is defined as follows. The trait is named Sliceable, and the resulting view type is Slice<T> (2.9.7). The names are split because under the implicit glob one name cannot refer to both.

#[lang("slice_trait")]
trait Sliceable<R> {
    type Output;

    fn slice(self, range: R) -> Output;
}

xs[a:b] is xs.slice(a..b), and xs[a:b:k] is xs.slice(..).step(k) over the range computed by Python's slice.indices (refs-lang 3.6.2). The semantics match Python slices: bounds are clamped to the length, out-of-range bounds never panic, and xs is evaluated once. xs[a..b] and xs[..] are the same. Every Sliceable<Range<usize>> implementation clamps its range with rok::core::index::clamp_range.

let xs: List<i32> = [1, 2, 3, 4, 5];
let s = xs[1:3];       //→ Slice<i32> view of [2, 3]
let r = xs[::2];       //→ [1, 3, 5] (step=2)
let v = xs[::-1];      //→ [5, 4, 3, 2, 1] (reversed)

let str_part = "hello"[1:4];  //→ "ell" (new str)

The standard library implements Sliceable<Range<usize>> for List<T>, Slice<T>, [T; N], str and StrBuf. List / Slice / arrays return a Slice<T> view; str / StrBuf return a new str copy after checking UTF-8 boundaries. Negative bounds count from the end and are then clamped; slice assignment is not supported. For an owned copy, use xs[1:3].to_list().

2.3.5. Trait implementation matrix

Every ops trait impl for primitive types carries #[intrinsic("rok.OP.TYPE")] and binds directly to a compiler backend PrimOp (0.3). This means:

  • An operator on primitive operands (a + b) is lowered directly to a machine operation without a method call.
  • A call by name (a.add(b), Add::add(a, b)) is lowered to the same machine operation.

Arithmetic traits

TypeModuleBehavior
i8..i64, isizerok::core::numrok.add.i32 / rok.sub.i32 / rok.rem.i32 / rok.mod.i32 ...
u8..u64, usizerok::core::numrok.add.u32 / ... (unsigned family, Modulo ≡ Rem)
f32, f64rok::core::numrok.add.f32 / rok.add.f64 / rok.mod.f64 (floored)
introk::core::numauto-promote logic
floatrok::core::num(same as above - auto-promote)

Bitwise traits

TypeModuleintrinsic
i*, u*, boolrok::core::numrok.and.OP, rok.or.OP, rok.xor.OP, rok.shl.OP, rok.shr.OP

f32 and f64 do not implement BitAnd and the like, since they are meaningless there.

Indexing traits

TypeDefined by
List<T>Standard library
Dict<K, V>Standard library (Idx = K)
[T; N]Compiler-synthesized (primitive array)
strStandard library (Slice only - Index<usize> undefined, direct byte access rejected)
Slice<T>Standard library
TupleCompiler-synthesized (.0, .1 form) - Index undefined (compile-time tuple access only)

2.3.6. User-defined ops

Users outside the standard library may freely implement the traits through impl Add for MyType and the like. Trait dispatch then leads to a call of MyType::add.

struct Vec3 { x: f64; y: f64; z: f64; }

impl Add for Vec3 {
    type Output = Vec3;

    fn add(self, rhs: Vec3) -> Vec3 {
        Vec3 {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
            z: self.z + rhs.z,
        }
    }
}

let a = Vec3 { x: 1.0, y: 2.0, z: 3.0 };
let b = Vec3 { x: 0.5, y: 0.5, z: 0.5 };
let c = a + b;        //→ Vec3 { x: 1.5, y: 2.5, z: 3.5 }

Attaching #[intrinsic] to user ops, however, is forbidden (refs-standard-libraries 0.3.5), because the body is authoritative.

2.3.7. Relationships between traits - Output consistency

In an expression combining several traits (a + b * c), the Output types must be consistent.

let r = a + b * c;
//   = Add::add(a, Mul::mul(b, c))
//   requires: <typeof(b) as Mul<typeof(c)>>::Output compatible with Add's Rhs

Every primitive arithmetic impl in the standard library uses Output = Self. User types may define it freely, but Output = Self is recommended for tool friendliness, such as compiler error messages.

2.3.8. Relationship to the broadcast traits

a + b dispatches to Add::add and a .+ b dispatches to DotAdd::dot_add, so the two operations are handled by distinct traits. Ordinary arithmetic types (Vec3 and the like) usually implement both. The details are in 2.6 broadcast.

2.3.9. Logical traits - LogicalAnd (&&) / LogicalOr (||)

&& and || are connected to trait methods in the same way as other operators. Precedence and associativity are fixed by the refs-lang 3.2 table.

#[lang("logical_and_trait")]
trait LogicalAnd<Rhs = Self> {
    #[infix("&&")]
    fn logical_and(self, rhs: Rhs) -> bool;
}

#[lang("logical_or_trait")]
trait LogicalOr<Rhs = Self> {
    #[infix("||")]
    fn logical_or(self, rhs: Rhs) -> bool;
}

The standard library implements these two traits only for bool. When both operands are bool, the compiler identifies this implementation through the logical_and_trait / logical_or_trait slots and lowers it to a short-circuit branch instead of a method call - a && b does not evaluate b if a is false.

Warning

If a user type implements LogicalAnd / LogicalOr, a && b becomes an ordinary trait method call, so both operands are evaluated (no short-circuit). This is the same rule as the broadcast forms .&& / .|| (2.6). The language guarantees short-circuit only for bool operands.

2.3.10. Unicode math operator traits

The Unicode operators of refs-lang 1.10.3 are connected to the following traits. Every method has an ASCII name, so it can be called as a method instead of an operator. Precedence is fixed by the refs-lang 3.2 table. ÷ is a synonym of Div above, and ≡ ≠ ≤ ≥ are synonyms from 2.5 cmp.

trait Cross<Rhs = Self> {               // a × b - level 8
    type Output;
    #[infix("×")]
    fn cross(self, rhs: Rhs) -> Output;
}

trait Dot<Rhs = Self> {                 // a ⋅ b - level 8
    type Output;
    #[infix("⋅")]
    fn dot(self, rhs: Rhs) -> Output;
}

trait Compose<Rhs> {                    // f ∘ g - level 9, (f ∘ g)(x) = f(g(x))
    type Output;
    #[infix("∘")]
    fn compose(self, rhs: Rhs) -> Output;
}

trait Sqrt {                            // √x - prefix unary
    type Output;
    #[prefix("√")]
    fn sqrt(self) -> Output;
}

trait Contains<Item> {                  // a ∈ s, a ∉ s - level 4
    #[infix("∈", swap)]
    fn contains(self, item: Item) -> bool;
    #[infix("∉", swap)]
    fn not_contains(self, item: Item) -> bool { !self.contains(item) }
}

trait Subset<Rhs = Self> {              // a ⊂ b, a ⊆ b - level 4
    #[infix("⊂")]
    fn is_proper_subset(self, rhs: Rhs) -> bool;
    #[infix("⊆")]
    fn is_subset(self, rhs: Rhs) -> bool;
}

trait Union<Rhs = Self> {               // a ∪ b - level 7
    type Output;
    #[infix("∪")]
    fn union(self, rhs: Rhs) -> Output;
}

trait Intersect<Rhs = Self> {           // a ∩ b - level 8
    type Output;
    #[infix("∩")]
    fn intersection(self, rhs: Rhs) -> Output;
}

∈ ∉ take the container on the right, so they are connected with swap (refs-lang 6.7.2). a ∈ xs is xs.contains(a).

TraitStandard library implementations
Sqrtf32, f64 (#[intrinsic("rok.sqrt.TYPE")])
ContainsList<T>, Set<T>, Dict<K, V> (keys), str (substring), Range<T>
Subset, Union, IntersectSet<T>
ComposeFunction types fn(B) -> C and fn(A) -> B
Cross, DotNo standard library implementation - implemented by vector types

Edit this page on GitHub