dscale 0.7.1

A fast & deterministic simulation framework for benchmarking and testing distributed systems
Documentation
// DScale: deterministic distributed systems simulator
// Copyright (C) 2026  Konstantin Shprenger

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use std::{any::Any, fmt::Debug, sync::Arc};

fn static_zst<T: Message + 'static>() -> MessagePtr {
    let reference: &'static T = unsafe { &*std::ptr::NonNull::<T>::dangling().as_ptr() };
    MessagePtr::Static(reference)
}

/// Trait for values that can be sent between simulated processes.
///
/// The optional [`Message::virtual_size`] method controls bandwidth simulation.
/// If bandwidth is unbounded, the default (zero) is fine.
pub trait Message: Any + Send + Sync + Debug {
    /// Size in bytes used for bandwidth simulation.
    /// Can exceed the real memory footprint to model heavy payloads.
    /// Defaults to 0 (no bandwidth cost).
    fn virtual_size(&self) -> usize {
        usize::default()
    }
}

/// Reference-counted wrapper around a [`Message`].
///
/// Provides type-safe downcasting to recover the concrete message type.
/// For zero-sized messages, avoids heap allocation entirely.
#[derive(Clone, Debug)]
pub enum MessagePtr {
    Shared(Arc<dyn Message>),
    Static(&'static dyn Message),
}

impl MessagePtr {
    /// Creates a new `MessagePtr`. For zero-sized types this avoids heap allocation
    /// by using a static reference — ZSTs have no data, only the vtable pointer matters.
    pub fn new<T: Message + 'static>(msg: T) -> Self {
        if std::mem::size_of::<T>() == 0 {
            // Each monomorphization of this function gets its own static instance.
            // ZSTs have no data, so the static is just a vtable anchor — zero heap allocation.
            static_zst::<T>()
        } else {
            MessagePtr::Shared(Arc::new(msg))
        }
    }

    fn as_dyn(&self) -> &dyn Message {
        match self {
            MessagePtr::Shared(arc) => &**arc,
            MessagePtr::Static(r) => *r,
        }
    }

    /// Attempts to downcast to `T`. Returns `None` if the type does not match.
    pub fn try_as_type<T: 'static>(&self) -> Option<&T> {
        (self.as_dyn() as &dyn Any).downcast_ref::<T>()
    }

    /// Returns `true` if the contained message is of type `T`.
    pub fn is<T: 'static>(&self) -> bool {
        self.try_as_type::<T>().is_some()
    }

    /// Downcasts to `T`, panicking if the type does not match.
    pub fn as_type<T: 'static>(&self) -> &T {
        self.try_as_type::<T>().expect("failed as_type")
    }

    /// Returns the virtual size of the message for bandwidth simulation.
    pub fn virtual_size(&self) -> usize {
        self.as_dyn().virtual_size()
    }
}