odem-rs-core 0.3.0

Core components of the odem-rs simulation framework
Documentation
//! Provides type-erased and object-safe traits that can be used to dynamically
//! dispatch over [`Simulators`], regardless of their [configuration].
//!
//! [`Simulators`]: crate::simulator::Simulator
//! [configuration]: Config

pub use crate::continuation::erased::State;

use core::{any::Any, fmt, ptr::NonNull, task::RawWaker};

use crate::{
	config::Config,
	continuation,
	ptr::{IntrusivelyCounted, Irc},
	simulator::Prec,
};

/// Type-erased trait for [Simulation contexts](crate::simulator::Sim).
pub trait Sim: Any + IntrusivelyCounted {
	/// Returns the currently active and config-erased version of a [`Puck`].
	///
	/// [`Puck`]: crate::Puck
	fn active(&self) -> Option<ContPuck>;

	/// Returns the current time as a displayable object.
	fn now(&self) -> &dyn fmt::Display;

	/// Returns the global (shared) data of the simulator.
	fn global(&self) -> &dyn Any;

	/// Constructs a [`RawWaker`] for the currently active process.
	fn waker(&self) -> RawWaker;

	/// Defers the currently active process.
	fn defer(&self);
}

/// Type-erased trait for [continuations](continuation::Continuation).
pub trait Continuation: Any + IntrusivelyCounted + fmt::Debug {
	/// Returns a type-erased reference to the shared agent data.
	fn subject(&self) -> &dyn Any;

	/// Returns the continuation's [`Label`](continuation::Label).
	fn label(&self) -> continuation::Label;

	/// Returns the continuation's [precedence](Prec).
	fn prec(&self) -> Prec;

	/// Returns the continuation's [`State`].
	fn state(&self) -> State;
}

/// [`Config`]-erased type for [pucks](continuation::Puck).
#[derive(Clone)]
pub struct ContPuck(Irc<dyn Continuation>);

impl ContPuck {
	/// Converts a [`Config`]-erased [`ContPuck`] into a `Config`-dependent
	/// [Puck](continuation::Puck) or returns the original object if the
	/// configuration didn't agree with the actual type.
	pub fn downcast<C: Config>(self) -> Result<continuation::Puck<C>, Self> {
		match self.0.downcast::<continuation::Continuation<'static, C>>() {
			Ok(task) => Ok(continuation::Puck::checked(task).unwrap()),
			Err(old) => Err(ContPuck(old)),
		}
	}

	/// Returns a reference to the instance shared among all the [jobs]
	/// associated with the same agent as this one.
	///
	/// [jobs]: crate::job::Job
	pub fn shared(&self) -> &dyn Any {
		self.0.subject()
	}

	/// Returns the name of the underlying [`Agent`](crate::agent::Agent).
	pub fn label(&self) -> continuation::Label {
		self.0.label()
	}

	/// Returns the current [precedence] of the underlying [Job].
	///
	/// [precedence]: Prec
	/// [Job]: crate::job::Job
	pub fn prec(&self) -> Prec {
		self.0.prec()
	}

	/// Returns a copy of this continuation's [`State`].
	pub fn state(&self) -> State {
		self.0.state()
	}
}

impl<C: Config> From<continuation::Puck<C>> for ContPuck {
	fn from(task: continuation::Puck<C>) -> Self {
		let coerced = Irc::into_raw(task.into_inner()) as NonNull<dyn Continuation>;
		ContPuck(unsafe { Irc::from_raw(coerced) })
	}
}

impl fmt::Debug for ContPuck {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		self.0.fmt(f)
	}
}

/// Currently active thread-local and type-erased [simulation context].
///
/// [simulation context]: Sim
use shared::SIMULATOR;

/// Sets the passed [simulation context](crate::simulator::Sim)-reference as the
/// new active simulator, returning a [Guard] that resets the active simulator
/// on drop.
#[must_use = "dropping this guard restores the previous thread-local simulator"]
pub(crate) fn hook(sim: Irc<impl Sim>) -> impl Drop {
	scopeguard::guard(
		SIMULATOR.replace(Some(Irc::map(sim, |inner| -> &dyn Sim { inner }))),
		|sim| {
			SIMULATOR.replace(sim);
		},
	)
}

/// Executes a closure receiving a type-erased [simulation context].
/// Returns `None` if no simulator is currently active.
///
/// [simulation context]: crate::simulator::Sim
pub fn with<R>(f: impl FnOnce(&dyn Sim) -> R) -> Option<R> {
	SIMULATOR.with(|inner| {
		let sim = inner.take()?;
		let res = f(&*sim);
		inner.set(Some(sim));
		Some(res)
	})
}

/// Returns a [`ContPuck`] for the currently active continuation if this thread
/// is currently running a simulation.
pub fn active() -> Option<ContPuck> {
	SIMULATOR.with(|inner| {
		let sim = inner.take()?;
		let res = sim.active();
		inner.set(Some(sim));
		res
	})
}

/* *************************************************************** Model Time */

/// Helper that allows displaying the model time of the currently active
/// simulator in human-readable format.
pub struct ModelTime<W>(pub W)
where
	W: Fn(&mut dyn fmt::Write, &dyn Sim) -> fmt::Result;

impl<W> fmt::Display for ModelTime<W>
where
	W: Fn(&mut dyn fmt::Write, &dyn Sim) -> fmt::Result,
{
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		with(move |sim| self.0(f, sim)).unwrap_or(Ok(()))
	}
}

/// Constructs a constant [`ModelTime`] instance that may be used to return the
/// model time of the currently active simulator for this thread.
///
/// The macro accepts a format string and optional, additional arguments as
/// specified for the [format_args]-macro. The format string may reference the
/// `time`-variable.
///
/// This is really meant to be used in combination with the `tracing`-crate, but
/// it is possible to use independently through the [`Display`]-trait.
///
/// # Example
/// ```
/// # use odem_rs_core::{simulator::{Sim, simulation}, model_time};
/// # async fn sim_main(sim: &Sim) {}
/// tracing_subscriber::fmt()
///     .with_timer(model_time!("[{time:#}]"))
///     .init();
///
/// simulation(sim_main).unwrap();
/// ```
///
/// [`Display`]: fmt::Display
#[macro_export]
macro_rules! model_time {
	() => { $crate::model_time!("[{time}]") };

	($($arg:tt)*) => {
		$crate::erased::ModelTime(
			move |w,s| ::core::fmt::write(w, ::core::format_args!($($arg)*, time = s.now()))
		)
	};
}

// make ModelTime compatible to the FormatTime-trait from tracing-subscriber
#[cfg(feature = "tracing")]
#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
impl<W> tracing_subscriber::fmt::time::FormatTime for ModelTime<W>
where
	W: Fn(&mut dyn fmt::Write, &dyn Sim) -> fmt::Result,
{
	fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> fmt::Result {
		with(move |sim| self.0(w, sim)).unwrap_or(Ok(()))
	}
}

/* ****************************************** Shared Simulator Helper Modules */

#[cfg(feature = "std")]
mod shared {
	use super::{Irc, Sim};
	use std::{cell::Cell, thread_local};

	thread_local! {
		pub static SIMULATOR: Cell<Option<Irc<dyn Sim>>> = const { Cell::new(None) };
	}
}

#[cfg(not(feature = "std"))]
mod shared {
	use super::{Irc, Sim};
	use core::{
		cell::Cell,
		sync::atomic::{AtomicBool, Ordering},
	};

	pub static SIMULATOR: LocalKey = LocalKey::new();

	pub struct LocalKey {
		sim: Cell<Option<Irc<dyn Sim>>>,
		lock: AtomicBool,
	}

	impl LocalKey {
		const fn new() -> Self {
			Self {
				sim: Cell::new(None),
				lock: AtomicBool::new(true),
			}
		}

		pub fn with<R>(&self, f: impl FnOnce(&Cell<Option<Irc<dyn Sim>>>) -> R) -> R {
			assert!(
				self.lock.swap(false, Ordering::SeqCst),
				"detected multiple threads running simulators"
			);

			let res = f(&self.sim);

			self.lock.store(true, Ordering::SeqCst);
			res
		}

		pub fn replace(&self, sim: Option<Irc<dyn Sim>>) -> Option<Irc<dyn Sim>> {
			assert!(
				self.lock.swap(false, Ordering::SeqCst),
				"detected multiple threads running simulators"
			);

			let res = self.sim.replace(sim);

			self.lock.store(true, Ordering::SeqCst);
			res
		}
	}

	unsafe impl Sync for LocalKey {}
}