Skip to main content

starlang_core/
pid.rs

1//! Process identifier type.
2//!
3//! A [`Pid`] uniquely identifies a process within the Starlang runtime. It consists of
4//! three components, matching Erlang's PID format `<node.id.creation>`:
5//!
6//! - **node**: Which node the process belongs to (as an Atom for global uniqueness)
7//! - **id**: The unique process identifier within that node
8//! - **creation**: Distinguishes PIDs across node restarts
9//!
10//! The node is stored as an [`Atom`] (interned string) so that PIDs are globally
11//! unambiguous - `<node1@localhost.5.0>` means the same thing on any node.
12//!
13//! The creation number prevents stale PIDs from accidentally matching new processes
14//! after a node restart.
15
16use crate::node::node_name_atom;
17use serde::{Deserialize, Serialize};
18use starlang_atom::Atom;
19use std::fmt;
20use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
21
22/// Global counter for generating unique process IDs.
23static PID_COUNTER: AtomicU64 = AtomicU64::new(0);
24
25/// Current node creation number. Incremented on each node restart.
26static CREATION: AtomicU32 = AtomicU32::new(0);
27
28/// A process identifier.
29///
30/// Every process in Starlang has a unique `Pid` that can be used to send messages,
31/// establish links, create monitors, and query process status.
32///
33/// The node is stored as an [`Atom`] (interned string) for global uniqueness.
34/// Display format matches Erlang's `<node.id.creation>` convention, showing
35/// `<0.id.creation>` for local PIDs and `<N.id.creation>` for remote ones.
36///
37/// # Examples
38///
39/// ```
40/// use starlang_core::Pid;
41///
42/// let pid = Pid::new();
43/// println!("Process: {}", pid);  // e.g., "<0.42.0>"
44///
45/// // Pids can be compared
46/// let pid2 = Pid::new();
47/// assert_ne!(pid, pid2);
48/// ```
49#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub struct Pid {
51    /// Node identifier as an atom (e.g., "node1@localhost").
52    node: Atom,
53    /// Unique process identifier within the node.
54    id: u64,
55    /// Creation number - distinguishes PIDs across node restarts.
56    creation: u32,
57}
58
59impl Pid {
60    /// Creates a new unique process identifier on the local node.
61    ///
62    /// Each call to `new()` returns a globally unique `Pid`.
63    /// The node is set to the current node's name atom.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use starlang_core::Pid;
69    ///
70    /// let pid1 = Pid::new();
71    /// let pid2 = Pid::new();
72    /// assert_ne!(pid1, pid2);
73    /// ```
74    pub fn new() -> Self {
75        Self {
76            node: node_name_atom(),
77            id: PID_COUNTER.fetch_add(1, Ordering::Relaxed),
78            creation: CREATION.load(Ordering::Relaxed),
79        }
80    }
81
82    /// Creates a `Pid` with a specific node atom, id, and creation values.
83    ///
84    /// This is primarily used for deserialization and testing. In normal usage,
85    /// prefer [`Pid::new()`].
86    pub fn from_parts_atom(node: Atom, id: u64, creation: u32) -> Self {
87        Self { node, id, creation }
88    }
89
90    /// Creates a remote `Pid` for a process on another node.
91    ///
92    /// This is used when receiving PIDs from remote nodes during distribution.
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use starlang_core::Pid;
98    /// use starlang_atom::atom;
99    ///
100    /// let remote_pid = Pid::remote("node2@localhost", 100, 5);
101    /// assert!(!remote_pid.is_local());
102    /// ```
103    pub fn remote(node_name: &str, id: u64, creation: u32) -> Self {
104        Self {
105            node: Atom::new(node_name),
106            id,
107            creation,
108        }
109    }
110
111    /// Returns the node atom.
112    #[inline]
113    pub fn node(&self) -> Atom {
114        self.node
115    }
116
117    /// Returns the node name as a string.
118    #[inline]
119    pub fn node_name(&self) -> String {
120        self.node.as_str()
121    }
122
123    /// Returns the process identifier within the node.
124    #[inline]
125    pub const fn id(&self) -> u64 {
126        self.id
127    }
128
129    /// Returns the creation number.
130    ///
131    /// The creation number distinguishes PIDs across node restarts.
132    #[inline]
133    pub const fn creation(&self) -> u32 {
134        self.creation
135    }
136
137    /// Returns `true` if this is a local process.
138    ///
139    /// A PID is local if its node matches the current node's name.
140    #[inline]
141    pub fn is_local(&self) -> bool {
142        self.node == node_name_atom()
143    }
144}
145
146/// Increment the creation counter.
147///
148/// This should be called when the node restarts to invalidate old PIDs.
149/// Returns the new creation value.
150pub fn increment_creation() -> u32 {
151    CREATION.fetch_add(1, Ordering::SeqCst) + 1
152}
153
154/// Get the current creation value.
155pub fn current_creation() -> u32 {
156    CREATION.load(Ordering::Relaxed)
157}
158
159impl Default for Pid {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165impl fmt::Debug for Pid {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        if self.is_local() {
168            write!(f, "Pid<0.{}.{}>", self.id, self.creation)
169        } else {
170            // Show the node name for remote PIDs
171            write!(f, "Pid<{}.{}.{}>", self.node, self.id, self.creation)
172        }
173    }
174}
175
176impl fmt::Display for Pid {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        if self.is_local() {
179            write!(f, "<0.{}.{}>", self.id, self.creation)
180        } else {
181            // Show the node name for remote PIDs
182            write!(f, "<{}.{}.{}>", self.node, self.id, self.creation)
183        }
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use starlang_atom::atom;
191
192    #[test]
193    fn test_pid_uniqueness() {
194        let pid1 = Pid::new();
195        let pid2 = Pid::new();
196        assert_ne!(pid1, pid2);
197    }
198
199    #[test]
200    fn test_pid_local() {
201        let pid = Pid::new();
202        assert!(pid.is_local());
203    }
204
205    #[test]
206    fn test_pid_remote() {
207        let pid = Pid::remote("node2@localhost", 100, 2);
208        assert_eq!(pid.node_name(), "node2@localhost");
209        assert_eq!(pid.id(), 100);
210        assert_eq!(pid.creation(), 2);
211        assert!(!pid.is_local());
212    }
213
214    #[test]
215    fn test_pid_from_parts_atom() {
216        let node = atom!("test@host");
217        let pid = Pid::from_parts_atom(node, 42, 1);
218        assert_eq!(pid.node(), node);
219        assert_eq!(pid.id(), 42);
220        assert_eq!(pid.creation(), 1);
221    }
222
223    #[test]
224    fn test_pid_display_local() {
225        let pid = Pid::new();
226        // Local PIDs show as <0.id.creation>
227        let display = format!("{}", pid);
228        assert!(
229            display.starts_with("<0."),
230            "expected local display format, got: {}",
231            display
232        );
233        // Format is <0.id.creation> - verify structure
234        let parts: Vec<&str> = display
235            .trim_matches(|c| c == '<' || c == '>')
236            .split('.')
237            .collect();
238        assert_eq!(parts.len(), 3, "expected 3 parts in PID display");
239        assert_eq!(parts[0], "0", "expected node 0 for local PID");
240    }
241
242    #[test]
243    fn test_pid_display_remote() {
244        let pid = Pid::remote("node2@host", 42, 0);
245        assert_eq!(format!("{}", pid), "<node2@host.42.0>");
246        assert_eq!(format!("{:?}", pid), "Pid<node2@host.42.0>");
247    }
248
249    #[test]
250    fn test_pid_serialization() {
251        let pid = Pid::remote("node1@localhost", 123, 5);
252        let bytes = postcard::to_allocvec(&pid).unwrap();
253        let decoded: Pid = postcard::from_bytes(&bytes).unwrap();
254        assert_eq!(pid, decoded);
255        assert_eq!(decoded.node_name(), "node1@localhost");
256    }
257
258    #[test]
259    fn test_pid_hash() {
260        use std::collections::HashSet;
261
262        let mut set = HashSet::new();
263        let pid1 = Pid::new();
264        let pid2 = Pid::new();
265
266        set.insert(pid1);
267        set.insert(pid2);
268        set.insert(pid1); // duplicate
269
270        assert_eq!(set.len(), 2);
271    }
272
273    #[test]
274    fn test_creation_distinguishes_pids() {
275        // Same node and id but different creation should be different PIDs
276        let node = atom!("test@host");
277        let pid1 = Pid::from_parts_atom(node, 42, 0);
278        let pid2 = Pid::from_parts_atom(node, 42, 1);
279        assert_ne!(pid1, pid2);
280    }
281
282    #[test]
283    fn test_increment_creation() {
284        let before = current_creation();
285        let new_creation = increment_creation();
286        assert_eq!(new_creation, before + 1);
287        assert_eq!(current_creation(), new_creation);
288    }
289}