Skip to content

Commit 00fb52c

Browse files
PLeVasseurfelix91gr
authored andcommitted
Add guideline for issue #156
Co-authored-by: felix91gr <[email protected]>
1 parent f4b4f89 commit 00fb52c

File tree

1 file changed

+159
-0
lines changed

1 file changed

+159
-0
lines changed

src/coding-guidelines/expressions.rst

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,3 +399,162 @@ Expressions
399399
/* ... */
400400
}
401401
402+
403+
.. guideline:: Integer shift shall only be performed through `checked_` APIs
404+
:id: gui_DRmTvyOtvdZh
405+
:category: required
406+
:status: draft
407+
:release: 1.7.0-latest
408+
:fls: fls_sru4wi5jomoe
409+
:decidability: decidable
410+
:scope: module
411+
:tags: numerics, reduce-human-error, maintainability, portability, surprising-behavior, subset
412+
413+
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>`_.
414+
415+
This rule applies to the following primitive types:
416+
417+
418+
* ``i8``
419+
* ``i16``
420+
* ``i32``
421+
* ``i64``
422+
* ``i128``
423+
* ``u8``
424+
* ``u16``
425+
* ``u32``
426+
* ``u64``
427+
* ``u128``
428+
* ``usize``
429+
* ``isize``
430+
431+
.. rationale::
432+
:id: rat_ElqKhoEBAPHl
433+
:status: draft
434+
435+
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>`_.
436+
437+
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.
438+
439+
Reason 1: inconsistent behavior
440+
===============================
441+
442+
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
443+
444+
``x << M``
445+
446+
Then, it will behave like this:
447+
448+
.. list-table::
449+
:header-rows: 1
450+
451+
* - **Compilation Mode**
452+
- **\ ``0 <= M < N``\ **
453+
- **\ ``M < 0``\ **
454+
- **\ ``N <= M``\ **
455+
* - Debug
456+
- Shifts normally
457+
- Panics
458+
- Panics
459+
* - Release
460+
- Shifts normally
461+
- Shifts by ``M mod N``
462+
- Shifts by ``M mod N``
463+
464+
465+
..
466+
467+
Note: the behavior is exactly the same for the ``>>`` operator.
468+
469+
470+
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.
471+
472+
Therefore, a consistently-behaved operation should be required for performing shifts.
473+
474+
Reason 2: programmer intent
475+
===========================
476+
477+
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.
478+
479+
Therefore, an API that restricts the length of the shift to the range ``[0, N - 1]`` should be used instead of the ``<<`` and ``>>`` operators.
480+
481+
The Solution
482+
============
483+
484+
The ideal solution for this exists in ``core``\ : ``checked_shl`` and ``checked_shr``.
485+
486+
``<T>::checked_shl(M)`` returns a value of type ``Option<T>``\ , in the following way:
487+
488+
489+
* If ``M < 0``\ , the output is ``None``
490+
* If ``0 <= M < N`` for ``T`` of ``N`` bits, then the output is ``Some(T)``
491+
* If ``N <= M``\ , the output is ``None``
492+
493+
This API has consistent behavior across ``Debug`` and ``Release``\ , and makes the programmer intent explicit, which effectively solves this issue.
494+
495+
.. non_compliant_example::
496+
:id: non_compl_ex_xTceuJc7RoTb
497+
:status: draft
498+
499+
As seen in the example below:
500+
501+
502+
* A ``Debug`` build **panics**\ ,
503+
*
504+
Whereas a ``Release`` build prints the values:
505+
506+
.. code-block::
507+
508+
61 << -1 = 2147483648
509+
61 << 4 = 976
510+
61 << 40 = 15616
511+
512+
This shows **Reason 1** prominently.
513+
514+
**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.
515+
516+
.. code-block:: rust
517+
518+
fn bad_shl(bits: u32, shift: i32) -> u32 {
519+
bits << shift
520+
}
521+
522+
let bits : u32 = 61;
523+
let shifts = vec![-1, 4, 40];
524+
525+
for sh in shifts {
526+
println!("{bits} << {sh} = {}", bad_shl(bits, sh));
527+
}
528+
529+
.. compliant_example::
530+
:id: compl_ex_dn4iJW2Qf7kQ
531+
:status: draft
532+
533+
As seen in the example below:
534+
535+
536+
* Both ``Debug`` and ``Release`` give the same exact output, which addresses **Reason 1**.
537+
* Shifting by negative values is impossible due to the fact that ``checked_shl`` only accepts unsigned integers as shift lengths.
538+
* Shifting by more than ``N - 1`` (\ ``N`` being the bit-length of the value being shifted) returns a ``None`` value:
539+
.. code-block::
540+
541+
61 << 4 = Some(976)
542+
61 << 40 = None
543+
544+
The last 2 observations show how this addresses **Reason 2**.
545+
546+
.. code-block:: rust
547+
548+
fn good_shl(bits: u32, shift: u32) -> Option<u32> {
549+
bits.checked_shl(shift)
550+
}
551+
552+
let bits : u32 = 61;
553+
// let shifts = vec![-1, 4, 40];
554+
// ^--- Would not typecheck, as checked_shl
555+
// only accepts positive shift amounts
556+
let shifts = vec![4, 40];
557+
558+
for sh in shifts {
559+
println!("{bits} << {sh} = {:?}", good_shl(bits, sh));
560+
}

0 commit comments

Comments
 (0)