diff --git a/src/coding-guidelines/types-and-traits.rst b/src/coding-guidelines/types-and-traits.rst index dd1109dc..3ede5564 100644 --- a/src/coding-guidelines/types-and-traits.rst +++ b/src/coding-guidelines/types-and-traits.rst @@ -6,60 +6,131 @@ Types and Traits ================ -.. guideline:: Avoid Implicit Integer Wrapping - :id: gui_xztNdXA2oFNB - :category: required +.. guideline:: Use strong types to differentiate between logically distinct values + :id: gui_xztNdXA2oFNC + :category: advisory :status: draft :release: 1.85.0;1.85.1 :fls: fls_cokwseo3nnr - :decidability: decidable + :decidability: undecidable :scope: module - :tags: numerics + :tags: types, safety, understandability - Code must not rely on Rust's implicit integer wrapping behavior that may occur in release - builds. Instead, explicitly handle potential overflows using the standard library's checked, - saturating, or wrapping operations. + Parameters and variables with logically distinct types must be statically distinguishable by the type system. + Use a newtype (e.g., ``struct Meters(u32);``) when: + + * Two or more quantities share the same underlying primitive representation but are logically distinct + * Confusing them would constitute a semantic error + * You need to improve type safety and encapsulation + * You need to enable trait-based behavior + * You need to establish new invariants + .. rationale:: - :id: rat_kYiIiW8R2qD1 + :id: rat_kYiIiW8R2qD2 :status: draft - In debug builds, Rust performs runtime checks for integer overflow and will panic if detected. - However, in release builds (with optimizations enabled), unless the flag `overflow-checks`_ is - turned on, integer operations silently wrap around on overflow, creating potential for silent - failures and security vulnerabilities. Note that overflow-checks only brings the default panic - behavior from debug into release builds, avoiding potential silent wrap arounds. Nonetheless, - abrupt program termination is usually not suitable and, therefore, turning this flag on must - not be used as a substitute of explicit handling. Furthermore, the behavior in release mode is - under consideration by the The Rust Language Design Team and in the future overflow checking - may be turned on by default in release builds (it is a `frequently requested change`_). + 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. + In particular: - .. _overflow-checks: https://github.com/rust-lang/rust/blob/master/src/doc/rustc/src/codegen-options/index.md#overflow-checks - .. _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 + * Prevents mixing logically distinct values. + Primitive types like ``u32`` or ``u64`` can represent lengths, counters, timestamps, durations, IDs, or other values. + Different semantic domains can be confused, leading to incorrect computations. + The Rust type system prevents such mistakes when semantics are encoded into distinct types. + * Improves static safety. + Statically distinct types allow the compiler to enforce domain distinctions. + Accidental swapping of parameters or returning the wrong quantity becomes a compile-time error. + * Improves readability and discoverability. + Intent-revealing names (``Meters``, ``Seconds``, ``UserId``) make code self-documenting. + Type signatures become easier to read and understand. + * Enables domain-specific trait implementations. + Statically distinct types allow you to implement ``Add``, ``Mul``, or custom traits in ways that match the domain logic. + Aliases cannot do this, because they are not distinct types. + * Supports API evolution. + Statically distinct types act as strong API contracts that can evolve independently from their underlying representations. - Safety-critical software requires consistent and predictable behavior across all build - configurations. Explicit handling of potential overflow conditions improves code clarity, - maintainability, and reduces the risk of numerical errors in production. + .. non_compliant_example:: + :id: non_compl_ex_PO5TyFsRTlWw + :status: draft + + This noncompliant example uses primitive types directly, leading to potential confusion between ``distance`` and ``time``. + Nothing prevents the caller from passing ``time`` as ``distance`` or vice-versa. + The units of each type are not clear from the function signature alone. + Mistakes compile cleanly and silently produce wrong results. + + .. code-block:: rust + + fn travel(distance: u32, time: u32) -> u32 { + distance / time + } + + fn main() { + let d = 100; + let t = = 10; + let _result = travel(t, d); // Compiles, but semantically incorrect + } .. non_compliant_example:: :id: non_compl_ex_PO5TyFsRTlWv :status: draft + This noncompliant example uses aliases instead of distinct types. + Aliases do not create new types, so the compiler cannot enforce distinctions between ``Meters`` and ``Seconds``. + + Aliases cannot do this, because they are not distinct types. + This noncompliant example uses primitive types directly, leading to potential confusion between ``distance`` and ``time``. + Nothing prevents the caller from passing ``time`` as ``distance`` or vice-versa. + The units of each type are not clear from the function signature alone. + Mistakes compile cleanly and silently produce wrong results. + .. code-block:: rust - fn calculate_next_position(current: u32, velocity: u32) -> u32 { - // Potential for silent overflow in release builds - current + velocity + type Meters = u32; + type Seconds = u32; + type MetersPerSecond = u32; + + fn travel(distance: Meters, time: Seconds) -> MetersPerSecond { + distance / time + } + + fn main() { + let d: Meters = 100; + let t: Seconds = 10; + let _result = travel(t, d); // Compiles, but semantically incorrect } .. compliant_example:: - :id: compl_ex_WTe7GoPu5Ez0 + :id: compl_ex_WTe7GoPu5Ez1 :status: draft + This compliant example uses newtypes to create distinct types for ``Meters``, ``Seconds``, and ``MetersPerSecond``. + The compiler enforces correct usage, preventing accidental swapping of parameters. + The function signature clearly conveys the intended semantics of each parameter and return value. + .. code-block:: rust - fn calculate_next_position(current: u32, velocity: u32) -> u32 { - // Explicitly handle potential overflow with checked addition - current.checked_add(velocity).expect("Position calculation overflowed") - } + use std::ops::Div; + + #[derive(Debug, Clone, Copy)] + struct Meters(u32); + + #[derive(Debug, Clone, Copy)] + struct Seconds(u32); + + #[derive(Debug, Clone, Copy)] + struct MetersPerSecond(u32); + + impl Div for Meters { + type Output = MetersPerSecond; + + fn div(self, rhs: Seconds) -> Self::Output { + MetersPerSecond(self.0 / rhs.0) + } + } + fn main() { + let d = Meters(100); + let t = Seconds(10); + let result = d / t; // Clean and type-safe! + println!("{:?}", result); // MetersPerSecond(10) + }