Crate ranged_integers

source ·
Expand description

§Ranged integers [nightly only]

The crate provides an integer type restricted to a compile time defined range with automatic data size selection, automatic bounds calulation for arithmetics and the possibility of fixed-size array indexing and range iteration.

§Prerequisites

The library usage requires the following Rust features enabled in the user crate or application:

// Without this rustc generates errors and sometimes panics.
#![feature(adt_const_params, generic_const_exprs)]

// This features are for the compile-time arithmetics.
// Note that the crate sometimes causes ICEs with the feature(effects).
#![feature(const_trait_impl)]
#![feature(effects)]

§Usage and examples

§Ranged semantics

Use Ranged<MIN, MAX> type to be sure of the value range:

fn move_player(dice_roll: Ranged<1, 6>) {
    let x : i32 = dice_roll.into(); // Conversion is allowed, i32 can store 1..=6
}

§Contents

§Data layout paradigm

The Ranged automatically chooses the smallest size possible according to MIN..=MAX range. It supports i8, u8, i16, u16, i32, u32, i64, u64 and i128 layouts (u128 is not supported), and a special zero-size layout for “constant” values with MIN==MAX.

use core::mem::size_of;
assert_eq!(size_of::< Ranged<42, 42> >(), 0); // It's always 42, no need to store it
 
assert_eq!(size_of::< Ranged<-1, 127> >(), 1); // Fits i8
assert_eq!(size_of::< Ranged<0,  200> >(), 1); // Fits u8
assert_eq!(size_of::< Ranged<-1, 200> >(), 2); // Fits i16, doesn't fit i8 or u8
 
assert_eq!(size_of::< Ranged<0, 90000> >(),  4); // The range fits i32

The implementation heavily relies on the optimizer.

§Ranged and integer primitives

§Creation of Ranged at compile time

The Ranged::create_const can be used to create a Ranged value checking it at compile time. The macro r! does the same but a bit shorter.

// Way 1: specify the bounds explicitly
move_player(Ranged::<1,6>::create_const::<4>());
move_player(r!([1 6] 4));  // Same thing

// Way 2: do not specify the bounds when possible
move_player(Ranged::create_const::<4>());
move_player(r!([] 4));  // Same thing
let x: Ranged::<0, 100> = Ranged::create_const::<42>();
let y: Ranged::<0, 100> = r!([] 42);  // Same thing

// Way 3: a special case with the single possible value
let x = Ranged::<4, 4>::create_const::<4>();
let y = r!(4);  // Same thing

It fails if the bounds are corrupted:

move_player(r!([] 7)); // Error: Can't store 7 in [1 6] inverval
move_player(r!([1 7] 7)); // Error: type mismatch, move_player() requires Ranged<1, 6>

§Ranged -> Ranged conversion

The Ranged can be converted to the type with different bounds using expand() generic method (compile-time check) and the methods fit(), fit_min(), fit_max() for runtime check. The Ranged values may be compared to each other yielding the shrinked bounds using fit_less_than(), fit_less_eq(), fit_greater_than(), and fit_greater_eq() methods.

let expandable: Ranged<4, 5> = r!([] 5);  // Fits Ranged<1,6> accepted by move_player
let overlapping: Ranged<4, 9> = r!([] 5);  // Doesn't fit, but the value 5 is acceptable
move_player(expandable.expand());
move_player(overlapping.fit().unwrap());

Shrinking with expand() is forbidden:

move_player(overlapping.expand());  // Error: the bounds 4..=9 can't fit in 1..=6

§int -> Ranged conversion

Way 1: ensure the bounds with Ranged::new(i128) -> Option<Ranged> function.

Note: due to incomplete features limitations, the function may fail to compile with the ‘trait bound is not satisfied’ if the bounds are not explicitly specified.

let some_int = 4;
let some_wrong_int = 8;
assert!(Ranged::<0, 6>::new(some_int) == Some(r!([0 6] 4)));
assert!(Ranged::<0, 6>::new(some_wrong_int) == None);

move_player(Ranged::new(some_int).unwrap());  // this call may fail to compile
                                              // use Ranged::<0, 6>::new instead

Way 2: use the Remainder operation with the “const” divisor

let x: Ranged<-9, 9> = 15_i32 % r!(10);
let y: Ranged<0, 9> = 15_u32 % r!(10);
assert!(x == r!(5));
assert!(y == r!(5));

Way 3: Convert the primitive types to Ranged with their native bounds using AsRanged

use ranged_integers::AsRanged;
let x = 15_u8.as_ranged();  // Ranged<0, 255>
let y = 15_i16.as_ranged(); // Ranged<-32768, 32767>

§Ranged -> int conversion

int::From trait is implemented when the value is proved to fit into the result type:

let x = r!([0 200] 20);
let y: u8 = x.into();  // 0..=200 fits u8
let x = r!([0 200] 20);
let y: i8 = x.into();  // 0..=200 doesn't fit i8

From and Into operations can’t be used in const context. A set of const fns allows const conversions to any integer primitive except for u128:

let x = r!([0 200] 20);
let y = x.u8(); // y is u8
let z = x.i16(); // z is i16
let w = x.usize(); // w is usize
let x = r!([0 200] 20);
let err = x.i8();  // Error: 0..=200 doesn't fit i8

§Array indexing, slicing and iteration

The ConstInclusiveRange<MIN,MAX> zero-size type is a range MIN..=MAX capable to create the iterator (IntoIterator trait implemented) with Ranged<MIN, MAX> output type. The r! macro can be used instead.

The Ranged::iter_up method creates an iterator from the current value up to MAX.

The arrays [T; N] may be indexed with Ranged<0, {N-1}> and sliced with r!(MIN..END) range with a reference to fixed-size array output.

let arr = [r!([1 6] 2), r!([] 3), r!([] 4), r!([] 5)];

assert_eq!(arr[r!(1..3)], [3,4]);  // Slicing with array reference output
assert_eq!(arr[r!([0 3] 1)], 3);  // Slicing with array reference output

// Not recommended to use this:
    for i in ConstInclusiveRange::<0, 3> {
        move_player(arr[i])  // iters through 0,1,2,3
    }
for i in r!(0..4) {
    move_player(arr[i])  // iters through 0,1,2,3
}
for i in r!(0..=3) {
    move_player(arr[i])  // iters through 0,1,2,3
}
for mv in r!([1 6] 3).iter_up() {
    move_player(mv)  // calls with 3,4,5,6
}

§Comparison

All Eq and Ord operations between different Ranged types are allowed, so as Ranged vs integer comparisons:

assert!(r!([1 6] 4) == r!([1 10] 4));
assert!(r!([1 6] 4) != r!([1 6] 5));
assert!(r!(4) == 4);
assert!(5 != r!([1 6] 4));

assert!(r!(5) > r!([1 6] 4));
assert!(4 < r!([1 6] 5));

To constrain the output type ruled by comparison, one may use Ranged::fit_less_than function and its siblings Ranged::fit_less_eq, Ranged::fit_greater_than, and Ranged::fit_greater_eq.

§Arithmetics

The bounds of arithmetic operations results are automatically recalculated.

Currently supported:

let x = r!([1 6] 5);
let y = r!([1 6] 4);

let a = x + y;  // The minimum is (1+1)=2, the maximum is (6+6)=12
let check_add: Ranged<2, 12> = a;  // Range assertion assignment
assert_eq!(check_add, r!(9));

let s = x - y;  // The minimum is (1-6)=-5, the maximum is (6-1)=5
let check_sub: Ranged<-5, 5> = s;  // Range assertion assignment
assert_eq!(check_sub, r!(1));

let m = x * y;  // The minimum is (1*1)=1, the maximum is (6*6)=36
let check_mul: Ranged<1, 36> = m;  // Range assertion assignment
assert_eq!(check_mul, r!(20));

let d = x / y;  // The minimum is (1/6)=0, the maximum is (6/1)=6
let check_div: Ranged<0, 6> = d;  // Range assertion assignment
assert_eq!(check_div, r!(1));

let r = x % y;
let check_rem: Ranged<0, 5> = r;  // Range assertion assignment
assert_eq!(check_rem, r!(1));

let n = -x;
let check_neg: Ranged<-6, -1> = n;  // Range assertion assignment
assert_eq!(check_neg, r!(-5));
 
let min: Ranged<1,6> = x.min(a);
let max: Ranged<2,12> = x.max(a);
let abs: Ranged<0,6> = r!([-1 6] -1).abs();

The division and remainder are allowed only if it’s impossible to store “0” in the divisor:

let x = r!([1 6] 4);
let y = r!([0 6] 5);
let z = r!([-1 6] 5);

let d = x / y; // Error: y can be 0
let e = x % z; // Error: z can be 0

The true bounds calculation routine for Rem operation is far too complex. In this library the calculated bounds will never exceed 1-DMAXABS..=DMAXABS-1 where DMAXABS is the maximum of the divisor absolute value.

This kind of Rem followed by expand is available for any dividend:

let x = r!([-1000 1000] 500);
let y = r!([-1 1000] 500);
let d = r!([1 10] 7);

let r: Ranged<-9, 9> = (x%d).expand();
// In this case, it expands just from Ranged<-9, 9> to itself

let r: Ranged<-9, 9> = (y%d).expand();
// In this case, it expands from Ranged<-1, 9>

But the actual calculation routine can produce smaller bounds:

// In the general case the output is Ranged<1-MAX, MAX-1>, MAX from divisor (by absolute value)
let x: Ranged<-9, 9> = (r!([-1000 1000] 500) % r!([1 10] 7));
let x: Ranged<-9, 9> = (r!([-1000 1000] 500) % r!([-10 -1] -7));
 
// If the dividend is nonnegative or nonpositive,
// the output range is limited to 0.
let x: Ranged<0, 9> = r!([0 100] 15) % r!(10);
let x: Ranged<-9, 0> = r!([-100 0] -15) % r!(10);

// The limit can't exceed the dividend's MIN(if negative) or MAX(if positive):
let x: Ranged<-10, 10> = r!([-10 10] 4) % r!([1 1000] 70);

// If the divisor is "constant", the output bounds are the true bounds:
let x: Ranged<4, 7> = r!([14 17] 15) % r!(10);

// In particular, if both operands are "constant", the result is "constant"
let x: Ranged<5, 5> = r!(15) % r!(10);

Following these rules, the calculated bounds may be wider than the true ones, like Ranged<36289, 36292> % Ranged<6, 9> = Ranged<0, 8> while the result never exceeds Ranged<1, 4>.

§Pattern matching

A limited version is implemented with rmatch! macro.

Macros§

  • Create a ranged value or a range at compile time
  • Ranged pattern matching macro

Structs§

  • Const range for iterators with Ranged output and array indexing
  • A value restricted to the given bounds

Traits§

  • Convert an integer value to Ranged according to its own bounds.