Skip to content

Commit f5efaf5

Browse files
felix91grgithub-actions[bot]
authored andcommitted
Add guideline for issue #156
Co-authored-by: felix91gr <[email protected]>
1 parent 3958527 commit f5efaf5

File tree

1 file changed

+148
-0
lines changed

1 file changed

+148
-0
lines changed

src/coding-guidelines/expressions.rst

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,3 +463,151 @@ Expressions
463463
/* ... */
464464
}
465465
466+
467+
.. guideline:: Integer shift shall only be performed through `checked_` APIs
468+
:id: gui_qpbGbIuLPZ7Y
469+
:category: required
470+
:status: draft
471+
:release: 1.7.0-latest
472+
:fls: fls_sru4wi5jomoe
473+
:decidability: decidable
474+
:scope: module
475+
:tags: numerics, reduce-human-error, maintainability, portability, surprising-behavior, subset
476+
477+
In particular, the user should only perform left shifts via the `\ ``checked_shl`` <https://doc.rust-lang.org/core/index.html?search=%22checked_shl%22>`_ function and right shifts via the `\ ``checked_shr`` <https://doc.rust-lang.org/core/index.html?search=%22checked_shr%22>`_ function. Both of these functions exist in `\ ``core`` <https://doc.rust-lang.org/core/index.html>`_.
478+
479+
This rule applies to the following primitive types:
480+
481+
482+
* ``i8``
483+
* ``i16``
484+
* ``i32``
485+
* ``i64``
486+
* ``i128``
487+
* ``u8``
488+
* ``u16``
489+
* ``u32``
490+
* ``u64``
491+
* ``u128``
492+
* ``usize``
493+
* ``isize``
494+
495+
.. rationale::
496+
:id: rat_HlPRN7ThS5F7
497+
:status: draft
498+
499+
This is a Subset rule, directly inspired by `INT34-C. Do not shift an expression by a negative number of bits or by greater than or equal to the number of bits that exist in the operand <https://wiki.sei.cmu.edu/confluence/x/ItcxBQ>`_.
500+
501+
In Rust these out-of-range shifts don't give rise to Undefined Behavior; however, they are still problematic in Safety Critical contexts for two reasons.
502+
503+
504+
*
505+
**Reason 1: inconsistent behavior**
506+
507+
The behavior of shift operations depends on the compilation mode. Say for example, that we have a number ``x`` of type ``uN``\ , and we perform the operation
508+
509+
``x << M``
510+
511+
Then, it will behave like this:
512+
513+
| **Compilation Mode** | **\ ``0 <= M < N``\ ** | **\ ``M < 0``\ ** | **\ ``N <= M``\ ** |
514+
|:--------------------:|:----------------:|:---------------------:|:-------------------:|
515+
| Debug | Shifts normally | Panics | Panics |
516+
| Release | Shifts normally | Shifts by ``M mod N`` | Shifts by ``M mod N`` |
517+
518+
..
519+
520+
Note: the behavior is exactly the same for the ``>>`` operator.
521+
522+
523+
Panicking in ``Debug`` is an issue by itself, however, a perhaps larger issue there is that its behavior is different from that of ``Release``. Such inconsistencies aren't acceptable in Safety Critical scenarios.
524+
525+
Therefore, a consistently-behaved operation should be required for performing shifts.
526+
527+
*
528+
**Reason 2: programmer intent**
529+
530+
There is no scenario in which it makes sense to perform a shift of negative length, or of more than ``N - 1`` bits. The operation itself becomes meaningless.
531+
532+
Therefore, an API that restricts the length of the shift to the range ``[0, N - 1]`` should be used instead of the ``<<`` and ``>>`` operators.
533+
534+
*
535+
**The Solution**
536+
537+
The ideal solution for this exists in ``core``\ : ``checked_shl`` and ``checked_shr``.
538+
539+
``<T>::checked_shl(M)`` returns a value of type ``Option<T>``\ , in the following way:
540+
541+
542+
* If ``M < 0``\ , the output is ``None``
543+
* If ``0 <= M < N`` for ``T`` of ``N`` bits, then the output is ``Some(T)``
544+
* If ``N <= M``\ , the output is ``None``
545+
546+
This API has consistent behavior across ``Debug`` and ``Release``\ , and makes the programmer intent explicit, which effectively solves this issue.
547+
548+
.. non_compliant_example::
549+
:id: non_compl_ex_vK9OqZVsPWcd
550+
:status: draft
551+
552+
As seen in the example below:
553+
554+
555+
* A ``Debug`` build **panics**\ ,
556+
*
557+
Whereas a ``Release`` build prints the values:
558+
559+
.. code-block::
560+
561+
61 << -1 = 2147483648
562+
61 << 4 = 976
563+
61 << 40 = 15616
564+
565+
This shows **Reason 1** prominently.
566+
567+
**Reason 2** is not seen in the code, because it is a reason of programmer intent: shifts by less than 0 or by more than ``N - 1`` (\ ``N`` being the bit-length of the value being shifted) are both meaningless.
568+
569+
.. code-block:: rust
570+
571+
fn bad_shl(bits: u32, shift: i32) -> u32 {
572+
bits << shift
573+
}
574+
575+
let bits : u32 = 61;
576+
let shifts = vec![-1, 4, 40];
577+
578+
for sh in shifts {
579+
println!("{bits} << {sh} = {}", bad_shl(bits, sh));
580+
}
581+
582+
.. compliant_example::
583+
:id: compl_ex_SPxWTeKJN3Su
584+
:status: draft
585+
586+
As seen in the example below:
587+
588+
589+
* Both ``Debug`` and ``Release`` give the same exact output, which addresses **Reason 1**.
590+
* Shifting by negative values is impossible due to the fact that ``checked_shl`` only accepts unsigned integers as shift lengths.
591+
* Shifting by more than ``N - 1`` (\ ``N`` being the bit-length of the value being shifted) returns a ``None`` value:
592+
.. code-block::
593+
594+
61 << 4 = Some(976)
595+
61 << 40 = None
596+
597+
The last 2 observations show how this addresses **Reason 2**.
598+
599+
.. code-block:: rust
600+
601+
fn good_shl(bits: u32, shift: u32) -> Option<u32> {
602+
bits.checked_shl(shift)
603+
}
604+
605+
let bits : u32 = 61;
606+
// let shifts = vec![-1, 4, 40];
607+
// ^--- Would not typecheck, as checked_shl
608+
// only accepts positive shift amounts
609+
let shifts = vec![4, 40];
610+
611+
for sh in shifts {
612+
println!("{bits} << {sh} = {:?}", good_shl(bits, sh));
613+
}

0 commit comments

Comments
 (0)