Skip to content

Commit 92b2434

Browse files
PLeVasseurfelix91gr
authored andcommitted
Add guideline for issue #156
Co-authored-by: felix91gr <felix91gr@users.noreply.github.com>
1 parent 1d5d163 commit 92b2434

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
@@ -330,3 +330,162 @@ Expressions
330330
/* ... */
331331
}
332332
333+
334+
.. guideline:: Integer shift shall only be performed through `checked_` APIs
335+
:id: gui_Vc9PhO9AOCtO
336+
:category: required
337+
:status: draft
338+
:release: 1.7.0-latest
339+
:fls: fls_sru4wi5jomoe
340+
:decidability: decidable
341+
:scope: module
342+
:tags: numerics, reduce-human-error, maintainability, portability, surprising-behavior, subset
343+
344+
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>`_.
345+
346+
This rule applies to the following primitive types:
347+
348+
349+
* ``i8``
350+
* ``i16``
351+
* ``i32``
352+
* ``i64``
353+
* ``i128``
354+
* ``u8``
355+
* ``u16``
356+
* ``u32``
357+
* ``u64``
358+
* ``u128``
359+
* ``usize``
360+
* ``isize``
361+
362+
.. rationale::
363+
:id: rat_yyFZKJASRiha
364+
:status: draft
365+
366+
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>`_.
367+
368+
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.
369+
370+
Reason 1: inconsistent behavior
371+
===============================
372+
373+
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
374+
375+
``x << M``
376+
377+
Then, it will behave like this:
378+
379+
.. list-table::
380+
:header-rows: 1
381+
382+
* - **Compilation Mode**
383+
- **\ ``0 <= M < N``\ **
384+
- **\ ``M < 0``\ **
385+
- **\ ``N <= M``\ **
386+
* - Debug
387+
- Shifts normally
388+
- Panics
389+
- Panics
390+
* - Release
391+
- Shifts normally
392+
- Shifts by ``M mod N``
393+
- Shifts by ``M mod N``
394+
395+
396+
..
397+
398+
Note: the behavior is exactly the same for the ``>>`` operator.
399+
400+
401+
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.
402+
403+
Therefore, a consistently-behaved operation should be required for performing shifts.
404+
405+
Reason 2: programmer intent
406+
===========================
407+
408+
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.
409+
410+
Therefore, an API that restricts the length of the shift to the range ``[0, N - 1]`` should be used instead of the ``<<`` and ``>>`` operators.
411+
412+
The Solution
413+
============
414+
415+
The ideal solution for this exists in ``core``\ : ``checked_shl`` and ``checked_shr``.
416+
417+
``<T>::checked_shl(M)`` returns a value of type ``Option<T>``\ , in the following way:
418+
419+
420+
* If ``M < 0``\ , the output is ``None``
421+
* If ``0 <= M < N`` for ``T`` of ``N`` bits, then the output is ``Some(T)``
422+
* If ``N <= M``\ , the output is ``None``
423+
424+
This API has consistent behavior across ``Debug`` and ``Release``\ , and makes the programmer intent explicit, which effectively solves this issue.
425+
426+
.. non_compliant_example::
427+
:id: non_compl_ex_BjKvTxVewttA
428+
:status: draft
429+
430+
As seen in the example below:
431+
432+
433+
* A ``Debug`` build **panics**\ ,
434+
*
435+
Whereas a ``Release`` build prints the values:
436+
437+
.. code-block::
438+
439+
61 << -1 = 2147483648
440+
61 << 4 = 976
441+
61 << 40 = 15616
442+
443+
This shows **Reason 1** prominently.
444+
445+
**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.
446+
447+
.. code-block:: rust
448+
449+
fn bad_shl(bits: u32, shift: i32) -> u32 {
450+
bits << shift
451+
}
452+
453+
let bits : u32 = 61;
454+
let shifts = vec![-1, 4, 40];
455+
456+
for sh in shifts {
457+
println!("{bits} << {sh} = {}", bad_shl(bits, sh));
458+
}
459+
460+
.. compliant_example::
461+
:id: compl_ex_R1R7jLdAqh6w
462+
:status: draft
463+
464+
As seen in the example below:
465+
466+
467+
* Both ``Debug`` and ``Release`` give the same exact output, which addresses **Reason 1**.
468+
* Shifting by negative values is impossible due to the fact that ``checked_shl`` only accepts unsigned integers as shift lengths.
469+
* Shifting by more than ``N - 1`` (\ ``N`` being the bit-length of the value being shifted) returns a ``None`` value:
470+
.. code-block::
471+
472+
61 << 4 = Some(976)
473+
61 << 40 = None
474+
475+
The last 2 observations show how this addresses **Reason 2**.
476+
477+
.. code-block:: rust
478+
479+
fn good_shl(bits: u32, shift: u32) -> Option<u32> {
480+
bits.checked_shl(shift)
481+
}
482+
483+
let bits : u32 = 61;
484+
// let shifts = vec![-1, 4, 40];
485+
// ^--- Would not typecheck, as checked_shl
486+
// only accepts positive shift amounts
487+
let shifts = vec![4, 40];
488+
489+
for sh in shifts {
490+
println!("{bits} << {sh} = {:?}", good_shl(bits, sh));
491+
}

0 commit comments

Comments
 (0)