starlang-core 0.1.0

Core types for Starlang - Erlang-style concurrency for Rust
Documentation
//! System messages for process lifecycle events.
//!
//! System messages are internal messages that processes receive in response
//! to lifecycle events like linked process exits, monitor notifications,
//! and timeouts.

use crate::{ExitReason, Pid, Ref};
use serde::{Deserialize, Serialize};

/// System-level messages delivered to processes.
///
/// These messages are generated by the runtime in response to process
/// lifecycle events. They are distinct from user-defined messages and
/// are handled specially by abstractions like GenServer.
///
/// # Examples
///
/// ```
/// use starlang_core::{SystemMessage, ExitReason, Pid, Ref};
///
/// // Exit signal from a linked process
/// let exit = SystemMessage::Exit {
///     from: Pid::new(),
///     reason: ExitReason::Normal,
/// };
///
/// // DOWN notification from a monitored process
/// let down = SystemMessage::Down {
///     monitor_ref: Ref::from_raw(42),
///     pid: Pid::new(),
///     reason: ExitReason::Error("crashed".to_string()),
/// };
///
/// // Timeout notification
/// let timeout = SystemMessage::Timeout;
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum SystemMessage {
    /// Exit signal from a linked process.
    ///
    /// When a linked process terminates, an `Exit` message is sent to all
    /// processes linked to it (if they have `trap_exit` enabled).
    ///
    /// If `trap_exit` is disabled, the receiving process will also terminate
    /// with the same reason (unless the reason is `Normal`).
    Exit {
        /// The process that exited.
        from: Pid,
        /// The reason for termination.
        reason: ExitReason,
    },

    /// Monitor notification that a monitored process terminated.
    ///
    /// Unlike exit signals, DOWN messages are always delivered as messages
    /// and never cause the receiving process to terminate.
    Down {
        /// The reference returned when the monitor was created.
        monitor_ref: Ref,
        /// The process that was being monitored.
        pid: Pid,
        /// The reason for termination.
        reason: ExitReason,
    },

    /// Timeout notification.
    ///
    /// Delivered when a receive timeout expires or when a GenServer's
    /// timeout period elapses without receiving any messages.
    Timeout,
}

impl SystemMessage {
    /// Creates a new Exit message.
    pub fn exit(from: Pid, reason: ExitReason) -> Self {
        SystemMessage::Exit { from, reason }
    }

    /// Creates a new Down message.
    pub fn down(monitor_ref: Ref, pid: Pid, reason: ExitReason) -> Self {
        SystemMessage::Down {
            monitor_ref,
            pid,
            reason,
        }
    }

    /// Returns `true` if this is an Exit message.
    pub fn is_exit(&self) -> bool {
        matches!(self, SystemMessage::Exit { .. })
    }

    /// Returns `true` if this is a Down message.
    pub fn is_down(&self) -> bool {
        matches!(self, SystemMessage::Down { .. })
    }

    /// Returns `true` if this is a Timeout message.
    pub fn is_timeout(&self) -> bool {
        matches!(self, SystemMessage::Timeout)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_exit_message() {
        let pid = Pid::new();
        let msg = SystemMessage::exit(pid, ExitReason::Normal);

        assert!(msg.is_exit());
        assert!(!msg.is_down());
        assert!(!msg.is_timeout());

        if let SystemMessage::Exit { from, reason } = msg {
            assert_eq!(from, pid);
            assert_eq!(reason, ExitReason::Normal);
        } else {
            panic!("expected Exit variant");
        }
    }

    #[test]
    fn test_down_message() {
        let r = Ref::from_raw(100);
        let pid = Pid::new();
        let msg = SystemMessage::down(r, pid, ExitReason::Killed);

        assert!(!msg.is_exit());
        assert!(msg.is_down());
        assert!(!msg.is_timeout());

        if let SystemMessage::Down {
            monitor_ref,
            pid: p,
            reason,
        } = msg
        {
            assert_eq!(monitor_ref, r);
            assert_eq!(p, pid);
            assert_eq!(reason, ExitReason::Killed);
        } else {
            panic!("expected Down variant");
        }
    }

    #[test]
    fn test_timeout_message() {
        let msg = SystemMessage::Timeout;

        assert!(!msg.is_exit());
        assert!(!msg.is_down());
        assert!(msg.is_timeout());
    }

    #[test]
    fn test_serialization() {
        let pid1 = Pid::new();
        let pid2 = Pid::new();
        let messages = vec![
            SystemMessage::exit(pid1, ExitReason::Normal),
            SystemMessage::down(Ref::from_raw(42), pid2, ExitReason::Error("test".into())),
            SystemMessage::Timeout,
        ];

        for msg in messages {
            let bytes = postcard::to_allocvec(&msg).unwrap();
            let decoded: SystemMessage = postcard::from_bytes(&bytes).unwrap();
            assert_eq!(msg, decoded);
        }
    }
}