Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 102 additions & 31 deletions src/coding-guidelines/types-and-traits.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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<Seconds> 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)
}