Types and Traits

Guideline: Use strong types to differentiate between logically distinct values gui_xztNdXA2oFNC
status: draft
tags: types, safety, understandability
category: advisory
decidability: undecidable
scope: module
release: 1.85.0;1.85.1

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: rat_kYiIiW8R2qD2
status: draft
parent needs: gui_xztNdXA2oFNC

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:

  • 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.

Non-Compliant Example: non_compl_ex_PO5TyFsRTlWw
status: draft
parent needs: gui_xztNdXA2oFNC

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.

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: non_compl_ex_PO5TyFsRTlWv
status: draft
parent needs: gui_xztNdXA2oFNC

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.

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: compl_ex_WTe7GoPu5Ez1
status: draft
parent needs: gui_xztNdXA2oFNC

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.

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)
 }
Guideline: Do not depend on function pointer identity gui_QbvIknd9qNF6
status: draft
tags: surprising-behavior
category: required
decidability: decidable
scope: system
release: unclear-latest

Do not rely on the equality or stable identity of function pointers.

Exception

#[no_mangle] functions are guaranteed to have a single instance.

Rationale: rat_kYiIiW8R2qD3
status: draft
parent needs: gui_QbvIknd9qNF6
child needs: rat_xcVE5Hfnbb2u

Functions may be instantiated multiple times. They may, for example, be instantiated every time they are referenced. Only #[no_mangle] functions are guaranteed to be instantiated a single time, but can cause undefined behavior if they share a symbol with other identifiers.

Avoid assumptions about low-level metadata (such as symbol addresses) unless explicitly guaranteed by the Ferrocene Language Specification (FLS). Function address identity is not guaranteed and must not be treated as stable. Rust’s fn type is a zero-sized function item promoted to a function pointer, whose address is determined by the compiler backend. When a function resides in a different crate or codegen-unit partitioning is enabled, the compiler may generate multiple distinct code instances for the same function or alter the address at which it is emitted.

Consequently, the following operations are unreliable for functions which are not #[no_mangle]:

  • Comparing function pointers for equality (fn1 == fn2) 1Code has comments. Press enter to view.

  • Assuming a unique function address

  • Using function pointers as identity keys (e.g., in maps, registries, matchers) 1Code has comments. Press enter to view.

  • Matching behavior based on function address unless you instruct the linker to put a (#[no_mangle]) function at a specific address

This rule applies even when the functions are semantically identical, exported as pub, or defined once in source form.

Compiler optimizations may cause function pointers to lose stable identity, for example:

  • Cross-crate inlining can produce multiple code instantiations

  • Codegen-unit separation can cause function emission in multiple codegen units

  • Identical function implementations may be automatically merged as an optimization. Functions that are equivalent based only on specific hardware semantics may be merged in the machine-specific backend. Merging may also be performed as link-time optimization.

This behavior has resulted in real-world issues, such as the bug reported in rust-lang/rust#117047, where function pointer comparisons unexpectedly failed because the function in question was instantiated multiple times.

Violating this rule may cause:

  • Silent logic failures: callbacks not matching, dispatch tables misbehaving.

  • Inappropriate branching: identity-based dispatch selecting wrong handler.

  • Security issues: adversary-controlled conditions bypassing function-based authorization/dispatch logic.

  • Nondeterministic behavior: correctness depending on build flags or incremental state.

  • Test-only correctness: function pointer equality passing in debug builds but failing in release/link-time optimization builds.

In short, dependence on function address stability introduces non-portable, build-profile-dependent behavior, which is incompatible with high-integrity Rust.

Rationale: rat_xcVE5Hfnbb2u
status: draft
parent needs: rat_kYiIiW8R2qD3

Compiler optimizations may cause function pointers to lose stable identity, for example:

  • Cross-crate inlining can produce multiple code instantiations

  • Codegen-unit separation can cause function emission in multiple codegen units

  • Identical function implementations may be automatically merged as an optimization. Functions that are equivalent based only on specific hardware semantics may be merged in the machine-specific backend. Merging may also be performed as link-time optimization.

This behavior has resulted in real-world issues, such as the bug reported in rust-lang/rust#117047, where function pointer comparisons unexpectedly failed because the function in question was instantiated multiple times.

Violating this rule may cause:

  • Silent logic failures: callbacks not matching, dispatch tables misbehaving.

  • Inappropriate branching: identity-based dispatch selecting wrong handler.

  • Security issues: adversary-controlled conditions bypassing function-based authorization/dispatch logic.

  • Nondeterministic behavior: correctness depending on build flags or incremental state.

  • Test-only correctness: function pointer equality passing in debug builds but failing in release/link-time optimization builds.

In short, dependence on function address stability introduces non-portable, build-profile-dependent behavior, which is incompatible with high-integrity Rust.

Non-Compliant Example: non_compl_ex_MkAkFxjRTijx
status: draft
parent needs: gui_QbvIknd9qNF6

Due to cross-crate inlining or codegen-unit partitioning, the address of handler_a in crate B may differ from its address in crate A, causing comparisons to fail as shown in this noncompliant code example:

// crate A
pub fn handler_a() {}
pub fn handler_b() {}

// crate B
use crate_a::{handler_a, handler_b};

fn dispatch(f: fn()) {
    if f == handler_a {
        println!("Handled by A");
    } else if f == handler_b {
        println!("Handled by B");
    }
}

dispatch(handler_a);

//  Error:  This may fail unpredictably if handler_a is inlined or duplicated.
Compliant Example: compl_ex_oiqSSclTXmIi
status: draft
parent needs: gui_QbvIknd9qNF6

Replace function pointer comparison with an explicit enum as shown in this compliant example:

// crate A
pub enum HandlerId { A, B }

pub fn handler(id: HandlerId) {
    match id {
        HandlerId::A => handler_a(),
        HandlerId::B => handler_b(),
    }
}

// crate B
use crate_a::{handler, HandlerId};

fn dispatch(id: HandlerId) {
    handler(id);
}

dispatch(HandlerId::A);  // OK: semantically stable identity
Non-Compliant Example: non_compl_ex_MkAkFxjRTijy
status: draft
parent needs: gui_QbvIknd9qNF6

A function pointer used as a key is not guaranteed to have stable identity, as shown in this noncompliant example:

// crate A
pub fn op_mul(x: i32) -> i32 { x * 2 }

// crate B
use crate_a::op_mul;
use std::collections::HashMap;

let mut registry: HashMap<fn(i32) -> i32, &'static str> = HashMap::new();
registry.insert(op_mul, "double");

let f = op_mul;

// Error: Lookup may fail if `op_mul` has multiple emitted instances.
assert_eq!(registry.get(&f), Some(&"double"));
Compliant Example: compl_ex_oiqSSclTXmIj
status: draft
parent needs: gui_QbvIknd9qNF6

This compliant example uses a stable identity wrappers as identity keys. The id is a stable, programmer-defined identity, immune to compiler optimizations. The function pointer is preserved for behavior (func) but never used as the identity key.

// crate A

pub fn op_mul(x: i32) -> i32 { x * 2 }
pub fn op_add(x: i32) -> i32 { x + 2 }

// Stable identity wrapper for an operation.
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct Operation {
    pub id: u32,
    pub func: fn(i32) -> i32,
}

// Export stable descriptors.
pub const OP_MUL: Operation = Operation { id: 1, func: op_mul };
pub const OP_ADD: Operation = Operation { id: 2, func: op_add };

// crate B

use crate_a::{Operation, OP_MUL, OP_ADD};
use std::collections::HashMap;

fn main() {
  let mut registry: HashMap<u32, &'static str> = HashMap::new();

  // Insert using stable identity key (ID), not function pointer.
  registry.insert(OP_MUL.id, "double");
  registry.insert(OP_ADD.id, "increment");

  // Later: lookup using ID
  let op = OP_MUL;

  // lookup works reliably regardless of inlining, LTO, CGUs, cross-crate instantiation, etc.
  assert_eq!(registry.get(&op.id), Some(&"double"));

  println!("OP_MUL maps to: {}", registry[&op.id]);
}
Non-Compliant Example: non_compl_ex_MkAkFxjRTijz
status: draft
parent needs: gui_QbvIknd9qNF6

This noncompliant example relies on function pointer identity for deduplication:

// crate B
let mut handlers: Vec<fn()> = Vec::new();

fn register(h: fn()) {
    if !handlers.contains(&h) {
        handlers.push(h);
    }
}

register(handler); // Error: may be inserted twice under some builds
Compliant Example: compl_ex_oiqSSclTXmIk
status: draft
parent needs: gui_QbvIknd9qNF6

This compliant example keeps identity-sensitive logic inside a single crate:

// crate A (single crate boundary)
#[inline(never)]
pub fn important_handler() {}

pub fn is_important(f: fn()) -> bool {
    // Safe because identity and comparison are confined to one crate,
    // and inlining is prohibited.
    f == important_handler
}