|
6 | 6 | Types and Traits |
7 | 7 | ================ |
8 | 8 |
|
9 | | -.. guideline:: Avoid Implicit Integer Wrapping |
| 9 | +.. guideline:: Use strong types to differentiate between logically distinct values |
10 | 10 | :id: gui_xztNdXA2oFNB |
11 | | - :category: required |
| 11 | + :category: advisory |
12 | 12 | :status: draft |
13 | 13 | :release: 1.85.0;1.85.1 |
14 | 14 | :fls: fls_cokwseo3nnr |
15 | | - :decidability: decidable |
| 15 | + :decidability: undecidable |
16 | 16 | :scope: module |
17 | | - :tags: numerics |
| 17 | + :tags: types, safety, clarity |
18 | 18 |
|
19 | | - Code must not rely on Rust's implicit integer wrapping behavior that may occur in release |
20 | | - builds. Instead, explicitly handle potential overflows using the standard library's checked, |
21 | | - saturating, or wrapping operations. |
| 19 | + Parameters and variables with logically distinct types must be statically distinguishable by the type system. |
22 | 20 |
|
| 21 | + Use a newtype (e.g., ``struct Meters(u32);``) when: |
| 22 | + |
| 23 | + * Two or more quantities share the same underlying primitive representation but are logically distinct |
| 24 | + * Confusing them would constitute a semantic error |
| 25 | + * You need to improve type safety and encapsulation |
| 26 | + * You need to enable trait-based behavior |
| 27 | + * You need to establish new invariants |
| 28 | + |
23 | 29 | .. rationale:: |
24 | 30 | :id: rat_kYiIiW8R2qD1 |
25 | 31 | :status: draft |
26 | 32 |
|
27 | | - In debug builds, Rust performs runtime checks for integer overflow and will panic if detected. |
28 | | - However, in release builds (with optimizations enabled), unless the flag `overflow-checks`_ is |
29 | | - turned on, integer operations silently wrap around on overflow, creating potential for silent |
30 | | - failures and security vulnerabilities. Note that overflow-checks only brings the default panic |
31 | | - behavior from debug into release builds, avoiding potential silent wrap arounds. Nonetheless, |
32 | | - abrupt program termination is usually not suitable and, therefore, turning this flag on must |
33 | | - not be used as a substitute of explicit handling. Furthermore, the behavior in release mode is |
34 | | - under consideration by the The Rust Language Design Team and in the future overflow checking |
35 | | - may be turned on by default in release builds (it is a `frequently requested change`_). |
36 | | - |
37 | | - .. _overflow-checks: https://github.com/rust-lang/rust/blob/master/src/doc/rustc/src/codegen-options/index.md#overflow-checks |
38 | | - .. _frequently requested change: https://lang-team.rust-lang.org/frequently-requested-changes.html#numeric-overflow-checking-should-be-on-by-default-even-in-release-mode |
39 | | - |
40 | | - Safety-critical software requires consistent and predictable behavior across all build |
41 | | - configurations. Explicit handling of potential overflow conditions improves code clarity, |
42 | | - maintainability, and reduces the risk of numerical errors in production. |
| 33 | + This rule ensures that parameters and variables convey intent directly through the type system to avoid accidental misuse of values with identical primitives but different semantics. |
| 34 | + In particular: |
| 35 | + * Prevents mixing logically distinct values. |
| 36 | + Primitive types like ``u32`` or ``u64`` can represent lengths, counters, timestamps, durations, IDs, or other values. |
| 37 | + Different semantic domains can be confused, leading to incorrect computations. |
| 38 | + The Rust type system prevents such mistakes when semantics are encoded into distinct types. |
| 39 | + * Improves static safety. |
| 40 | + Statically distinct types allow the compiler to enforce domain distinctions. |
| 41 | + Accidental swapping of parameters or returning the wrong quantity becomes a compile-time error. |
| 42 | + * Improves readability and discoverability. |
| 43 | + Intent-revealing names (``Meters``, ``Seconds``, ``UserId``) make code self-documenting. |
| 44 | + Type signatures become easier to read and understand. |
| 45 | + * Enables domain-specific trait implementations. |
| 46 | + Statically distinct types allow you to implement ``Add``, ``Mul``, or custom traits in ways that match the domain logic. |
| 47 | + Aliases cannot do this, because they are not distinct types. |
| 48 | + * Supports API evolution. |
| 49 | + Statically distinct types act as strong API contracts that can evolve independently from their underlying representations. |
43 | 50 |
|
44 | 51 | .. non_compliant_example:: |
45 | 52 | :id: non_compl_ex_PO5TyFsRTlWv |
46 | 53 | :status: draft |
47 | 54 |
|
| 55 | + This noncompliant example uses primitive types directly, leading to potential confusion between ``distance`` and ``time``. |
| 56 | + Nothing prevents the caller from passing ``time`` as ``distance`` or vice-versa. |
| 57 | + The units of each type are not clear from the function signature alone. |
| 58 | + Mistakes compile cleanly and silently produce wrong results. |
| 59 | + |
| 60 | + .. code-block:: rust |
| 61 | +
|
| 62 | + fn travel(distance: u32, time: u32) -> u32 { |
| 63 | + distance / time |
| 64 | + } |
| 65 | +
|
| 66 | + fn main() { |
| 67 | + let d: Meters = 100; |
| 68 | + let t: Seconds = 10; |
| 69 | + let _result = travel(t, d); // Compiles, but semantically incorrect |
| 70 | + } |
| 71 | +
|
| 72 | + .. non_compliant_example:: |
| 73 | + :id: non_compl_ex_PO5TyFsRTlWu |
| 74 | + :status: draft |
| 75 | + |
| 76 | + This noncompliant example uses aliases instead of distinct types. |
| 77 | + Aliases do not create new types, so the compiler cannot enforce distinctions between ``Meters`` and ``Seconds``. |
| 78 | + |
| 79 | + Aliases cannot do this, because they are not distinct types. |
| 80 | + This noncompliant example uses primitive types directly, leading to potential confusion between ``distance`` and ``time``. |
| 81 | + Nothing prevents the caller from passing ``time`` as ``distance`` or vice-versa. |
| 82 | + The units of each type are not clear from the function signature alone. |
| 83 | + Mistakes compile cleanly and silently produce wrong results. |
| 84 | + |
48 | 85 | .. code-block:: rust |
49 | 86 |
|
50 | | - fn calculate_next_position(current: u32, velocity: u32) -> u32 { |
51 | | - // Potential for silent overflow in release builds |
52 | | - current + velocity |
| 87 | + type Meters = u32; |
| 88 | + type Seconds = u32; |
| 89 | + type MetersPerSecond = u32; |
| 90 | +
|
| 91 | + fn travel(distance: Meters, time: Seconds) -> MetersPerSecond { |
| 92 | + distance / time |
| 93 | + } |
| 94 | +
|
| 95 | + fn main() { |
| 96 | + let d: Meters = 100; |
| 97 | + let t: Seconds = 10; |
| 98 | + let _result = travel(t, d); // Compiles, but semantically incorrect |
53 | 99 | } |
54 | 100 |
|
55 | 101 | .. compliant_example:: |
56 | 102 | :id: compl_ex_WTe7GoPu5Ez0 |
57 | 103 | :status: draft |
58 | 104 |
|
| 105 | + This compliant example uses newtypes to create distinct types for ``Meters``, ``Seconds``, and ``MetersPerSecond``. |
| 106 | + The compiler enforces correct usage, preventing accidental swapping of parameters. |
| 107 | + The function signature clearly conveys the intended semantics of each parameter and return value. |
| 108 | + |
59 | 109 | .. code-block:: rust |
60 | 110 |
|
61 | | - fn calculate_next_position(current: u32, velocity: u32) -> u32 { |
62 | | - // Explicitly handle potential overflow with checked addition |
63 | | - current.checked_add(velocity).expect("Position calculation overflowed") |
| 111 | + struct Meters(u32); |
| 112 | + struct Seconds(u32); |
| 113 | + struct MetersPerSecond(u32); |
| 114 | +
|
| 115 | + fn travel(distance: Meters, time: Seconds) -> MetersPerSecond { |
| 116 | + MetersPerSecond(distance.0 / time.0) |
64 | 117 | } |
65 | 118 |
|
| 119 | + fn main() { |
| 120 | + let d = Meters(100); |
| 121 | + let t = Seconds(10); |
| 122 | + let _result = travel(d, t); // Correct usage |
| 123 | + } |
0 commit comments