#range #ranged #integer #compiler #light #limit #clamp

light_ranged_integers

Ranged integers for stable Rust compiler, zero-dependencies and no unsafe code

4 releases

0.1.3 May 17, 2024
0.1.2 May 10, 2024
0.1.1 May 10, 2024
0.1.0 May 6, 2024

#124 in Programming languages

GPL-3.0-or-later

58KB
1K SLoC

Light ranged integers

Ranged integers for stable Rust compiler, with zero dependencies and zero unsafe code.

Why

The concept of constraining an integer variable to a specific range comes from solid languages like Ada, helping the creation of reliable software by preventing unintended out of range garbage values.

In Rust we have the ranged_integers package, which is really powerful, with automatic type size and automatic interval limits extension; however, it currently needs a Nightly compiler along with enabling some experimental compiler features, as it needs more advanced control of constant expressions and generics.

While having more compile time automation is welcome, some runtime checking is good enough for most situations. In fact, even in Ada range types are mostly runtime checked, excluding the Range declaration and the very first initialization. If runtime checking is good enough for Ada's high safety standards, it's probably good enough for your use case, but that's up to you to decide.

Compared to the Rust ranged_integers package, this library has less compile-automation features, but it works on a stable compiler version, has zero dependencies and a very small codebase.

Usage

A Range type guarantees that the contained number fits the range from MIN to MAX, extremes included. MIN must be smaller than MAX, or it will refuse to compile.

The library offers Range types, declared in this form

RangedU16<MIN, MAX, OpMode>

where:

  • RangedU16 indicates a ranged type that internally saves the number as a u16 (unsigned, 16bit)
  • MIN is the lower limit of the interval
  • MAX is the upper limit of the interval
  • OpMode selects how to behave when a number goes out of range
use light_ranged_integers::{RangedU16, op_mode::Panic};

// Create a value of 3 in a range from 1 to 6, extremes included
let n1 = RangedU16::<1,6, Panic>::new(3);

// This is checked at compile time
let n1 = RangedU16::<1,6>::new_const::<3>();

Let's take a more real-world use case: let's represent a day of a non-leap year. Each month has a different amount of days, and we want to guarantee non-valid day numbers can't be accepted, hence assuring the day-month combination is always valid.

use light_ranged_integers::RangedU8;

enum DayInYear
{
    January(RangedU8<1,31>),
    February(RangedU8<1,28>),
    March(RangedU8<1,31>),
    April(RangedU8<1,30>),
    May(RangedU8<1,31>),
    June(RangedU8<1,30>),
    July(RangedU8<1,31>),
    August(RangedU8<1,31>),
    September(RangedU8<1,30>),
    October(RangedU8<1,31>),
    November(RangedU8<1,30>),
    December(RangedU8<1,31>)
}

// Range validity is done at compile-time,
// but the check of the number 10 fitting it is done
// at runtime.
// Let's put April 10th
let day = DayInYear::April(
    RangedU8::new(10)
);

// As we know the value at compile time,
// we may want to check and guarantee the number fits
// the range at compile time too
let day = DayInYear::April(
    RangedU8::new_const::<10>()
);

Modes

When you declare a Ranged type, you can select a behaviour to apply when the number goes outside range. There are currently three modes: Panic, Clamp and Wrap.

Panic

Panic mode is the default. If a number falls outside the range, the program panics.

use light_ranged_integers::{RangedU16, op_mode::Panic};
// Create a ranged u16, interval [1,6], in Panic mode.
let n1 = RangedU16::<1,6, Panic>::new(5);

// As Panic mode is the default, you can also initialize using only the range
let n1 = RangedU16::<1,6>::new(5);

let res = n1+5;// This line will panic, as 5+5=10 is out of range

Clamp

In Clamp mode, a number not fitting the interval is clamped to its closest extreme. For example, a 10 will be clamped to 6 when you try to fit it into a [1, 6] interval.

use light_ranged_integers::{RangedU16, op_mode::Clamp};
// Create a ranged u16, interval [1,6], in Clamp mode.
let n1 = RangedU16::<1,6, Clamp>::new(3);
assert_eq!(n1,3);
assert_eq!(n1+400, 6);

// Here 10 is clamped to a 6 to fit into range
let n2 = RangedU16::<1,6, Clamp>::new_adjust(10);
assert_eq!(n2,6);

// We're in clamp mode, so 3+6=9 is clamped at 6 automatically
assert_eq!(n1+n2, 6)

Wrap

Wrap mode.

In this mode, out of range numbers are wrapped to fit the interval.

use light_ranged_integers::{RangedU16, op_mode::Wrap};

// Adding +1 will wrap the number at the beginning of the interval
let n1 = RangedU16::<3,6, Wrap>::new(6);
assert_eq!(n1+1, 3);

// Subtract 1 from MIN. Returns MAX
let n2 = RangedU16::<3,6,Wrap>::new(3);
assert_eq!(n2-1, 6);

// one full interval cycle
assert_eq!(n2-4, n2);

You can wrap the new value during the initialization of a range too

use light_ranged_integers::{RangedU16, op_mode::Wrap};
let n = RangedU16::<3,6,Wrap>::new_adjust(7);
assert_eq!(n, 3);

Additional safety restrictions

The library offers some additional safety restrictions.

Immutable range

Range limits are not expanded or shrinked automatically so you always know what the range is.

use light_ranged_integers::{RangedU16, op_mode::Clamp};
let n1 = RangedU16::<1,6, Clamp>::new(3);
let n2 = RangedU16::<1,6, Clamp>::new(6);

// Expand the limit of each operand to 1-12
// Then 3+6=9 fits the range and 9 is not clamped
assert_eq!(
    n1.limit::<1,12>()
        +
    n2.limit::<1,12>(),
    9
);

Result range

If we add a value with range [0,3] and one with range [-1,6], what should be the output range? Depending on the situation, the developer may want the former, the latter, or an entirely different one. As we can't assume what the desired range is, doing operation between different ranges is forbidden at compile time.

use light_ranged_integers::RangedU8;

// Does NOT compile, different ranges
// one is [0,1], the other is [0,2]
// we can't assume an output range
RangedU8::<0,1>::new(0) + RangedU8::<0,2>::new(0)

Different OpMode

The same applies when trying to do operations between Range types set in a different OpMode. For example, if one operand is in Clamp mode and the other is in Panic mode, what should the code do when the number is out of range? What would be the resulting OpMode, the former or the latter?

As we can't assume the desired OpMode to use, it's better to not compile at all.

use light_ranged_integers::{RangedU8, op_mode::{Clamp, Panic}};

// Does NOT compile:
// First operand is Panic mode,
// second operand is in Clamp mode
RangedU8::<0, 1, Panic>::new(0)
    +
    RangedU8::<0, 1, Clamp>::new(0);

Copyright

This software is free and open source software, as defined by its license, this is NOT public domain, make sure to respect the license terms. You can find the license text in the COPYING file.

Copyright © 2024 Massimo Gismondi

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.

Dependencies

This project has NO runtime dependencies.

It has two dev-dependencies, used only for tests:

  • the paste crate by David Tolnay, published under either of Apache License, Version 2.0 or MIT license at your option.
  • the deranged crate by Jacob Pratt, published under either of Apache License, Version 2.0 or MIT license at your option.

No runtime deps