-
Notifications
You must be signed in to change notification settings - Fork 18
Description
Chapter
Expressions
Guideline Title
Ensure that all Integer Conversions are Lossless
Category
Mandatory
Status
Draft
Release Begin
1.0.0
Release End
latest
FLS Paragraph ID
fls_1qhsun1vyarz
Decidability
Undecidable
Scope
Module
Tags
numerics, surprising-behavior, defect
Amplification
Whenever using the as
keyword to convert between integer types, the user must ensure that no information is lost.
The recommended way to do this is to forgo the usage of as
altogether, and instead:
- To use
into
andfrom
whenever available - these guarantee lossless conversion. - To use
try_into
andtry_from
everywhere else - these ensure the programmer handles the cases where the intended conversion might lose information.
If the intent is to truncate values, the remainder operator %
is to be used instead of a type cast.
If the intent is to reinterpret an unsigned (or signed) integer as its signed (or unsigned) counterpart, this should not be done through a type cast either. Instead, this conversion may be performed through the functions cast_signed
and cast_unsigned
, respectively.
Exception(s)
No response
Rationale
This is directly inspired by INT31-C. Ensure that integer conversions do not result in lost or misinterpreted data.
In Rust, casts (as
conversions) are well-defined, regardless of hardware and compiler implementation. However:
- Casts can implicitly truncate, round or otherwise lose information.
- They can also subtly change the value when going from eg
uN
toiN
:- The bits in this case are reinterpreted,
- And signed integers always follow 2's complement in Rust,
- So any
uN
greater in value thaniN::MAX
will be cast to a negativeiN
.
Since implicitly losing information is undesirable, specially when it happens in such a drastic way as a change of sign, this behavior must be avoided.
Non-Compliant Example - Prose
The first example implicitly converts the u8
values to their bit-identical i8
ones. The output of this program shows how the counter gets implicitly wrapped down to the negative domain:
Sent: 125. Received: 125
Sent: 126. Received: 126
Sent: 127. Received: 127
Sent: 128. Received: -128
Sent: 129. Received: -127
The second example implicitly converts the large u16
values to truncated u8
ones:
Sent: 254. Received: 254
Sent: 255. Received: 255
Sent: 256. Received: 0
Sent: 257. Received: 1
Sent: 258. Received: 2
Sent: 259. Received: 3
Non-Compliant Example - Code
First Example
fn i_require_signed_ints(input: i8) {
println!("Received: {input}");
}
fn i_count_up(from: u8, to: u8) {
let mut counter: u8 = from;
while counter < to {
print!("Sent: {counter}. ");
i_require_signed_ints(counter as i8);
counter += 1;
}
}
i_count_up(125, 130);
Second Example
fn i_require_small_ints(input: u8) {
println!("Received: {input}");
}
fn i_count_up(from: u16, to: u16) {
let mut counter: u16 = from;
while counter < to {
print!("Sent: {counter}. ");
i_require_small_ints(counter as u8);
counter += 1;
}
}
i_count_up(254, 260);
Compliant Example - Prose
The first example demonstrates how to avoid implicit conversions by guarding against the known limit of the type being cast to. Here, we avoid sending a potentially breaking input to the receiver:
Sent: 125. Received: 125
Sent: 126. Received: 126
Sent: 127. Received: 127
Counter not sent. Exceeded limit of receiver (LIMIT = 127, counter = 128)
Counter not sent. Exceeded limit of receiver (LIMIT = 127, counter = 129)
The second example showcases the usage of try_into
to guard against loss of information. Here, we avoid truncating unintentionally, and are also able to recover a string representation of the error:
Sent: 254. Received: 254
Sent: 255. Received: 255
Didn't send 256: out of range integral type conversion attempted.
Didn't send 257: out of range integral type conversion attempted.
Didn't send 258: out of range integral type conversion attempted.
Didn't send 259: out of range integral type conversion attempted.
Compliant Example - Code
First Example
fn i_require_signed_ints(input: i8) {
println!("Received: {input}");
}
fn i_count_up(from: u8, to: u8) {
let mut counter: u8 = from;
const LIMIT : u8 = i8::MAX as u8;
while counter < to {
if counter <= LIMIT {
print!("Sent: {counter}. ");
i_require_signed_ints(counter as i8);
} else {
println!("Counter not sent. Exceeded limit of receiver (LIMIT = {LIMIT}, counter = {counter})");
}
counter += 1;
}
}
i_count_up(125, 130);
Second Example
fn i_require_small_ints(input: u8) {
println!("Received: {input}");
}
fn i_count_up(from: u16, to: u16) {
let mut counter: u16 = from;
while counter < to {
match counter.try_into() {
Ok(small_int) => {
print!("Sent: {small_int}. ");
i_require_small_ints(small_int);
},
Err(error) => {
println!("Didn't send {counter}: {error}.")
}
}
counter += 1;
}
}
i_count_up(254, 260);