Skip to main content

zerodds_ast_core/
reference.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Three-valued references. Spec §2.
4
5use crate::arena::Id;
6use crate::id::NodeId;
7use alloc::string::String;
8use alloc::vec::Vec;
9use core::fmt;
10
11/// A reference from one node to another.
12///
13/// Deliberately not `Option<Id<T>>`: type discovery delivers types whose
14/// targets have not arrived yet, and "absent" and "not resolved yet" are
15/// different facts that both need representing. Spec §2.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Ref<T> {
18    /// Points at a live node.
19    Resolved(Id<T>),
20    /// Names a target that has not been resolved yet.
21    Pending(PendingRef),
22    /// Resolution was attempted and failed.
23    Broken(BreakCause),
24}
25
26impl<T> Ref<T> {
27    /// The handle, if resolved.
28    #[must_use]
29    pub const fn resolved(&self) -> Option<Id<T>> {
30        match self {
31            Self::Resolved(id) => Some(*id),
32            Self::Pending(_) | Self::Broken(_) => None,
33        }
34    }
35
36    /// Whether this reference points at a live node.
37    #[must_use]
38    pub const fn is_resolved(&self) -> bool {
39        matches!(self, Self::Resolved(_))
40    }
41
42    /// Whether resolution is still outstanding.
43    #[must_use]
44    pub const fn is_pending(&self) -> bool {
45        matches!(self, Self::Pending(_))
46    }
47
48    /// Whether resolution failed.
49    #[must_use]
50    pub const fn is_broken(&self) -> bool {
51        matches!(self, Self::Broken(_))
52    }
53
54    /// The lookup key, for the pending-reference index.
55    ///
56    /// `None` once the reference is resolved or broken. A broken
57    /// reference is not waiting on anything and must not be retried
58    /// implicitly — a diagnostic already reported must not silently
59    /// vanish. Spec §2.
60    #[must_use]
61    pub const fn pending_key(&self) -> Option<&PendingRef> {
62        match self {
63            Self::Pending(key) => Some(key),
64            Self::Resolved(_) | Self::Broken(_) => None,
65        }
66    }
67
68    /// The failure cause, if broken.
69    #[must_use]
70    pub const fn break_cause(&self) -> Option<&BreakCause> {
71        match self {
72            Self::Broken(cause) => Some(cause),
73            Self::Resolved(_) | Self::Pending(_) => None,
74        }
75    }
76}
77
78/// What an unresolved reference names, in whichever vocabulary its
79/// frontend uses. Spec §2.
80#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
81pub enum PendingRef {
82    /// IDL scoped name, including forward declarations.
83    ScopedName(ScopedName),
84    /// XTypes type identifier, from discovery over the wire.
85    TypeIdentifier(TypeIdentifierKey),
86    /// ROS 2 interface name.
87    RosTypeName(RosTypeName),
88    /// CORBA repository identifier.
89    RepositoryId(RepositoryId),
90    /// DDS-XML `idref` target.
91    XmlRef(XmlRefKey),
92}
93
94/// IDL scoped name.
95///
96/// Ordering is segment-wise so that indices built on it iterate
97/// deterministically.
98#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
99pub struct ScopedName {
100    absolute: bool,
101    segments: Vec<String>,
102}
103
104impl ScopedName {
105    /// Name rooted at the global scope (`::A::B`).
106    #[must_use]
107    pub const fn absolute(segments: Vec<String>) -> Self {
108        Self {
109            absolute: true,
110            segments,
111        }
112    }
113
114    /// Name resolved relative to the current scope (`A::B`).
115    #[must_use]
116    pub const fn relative(segments: Vec<String>) -> Self {
117        Self {
118            absolute: false,
119            segments,
120        }
121    }
122
123    /// Whether the name is rooted at the global scope.
124    #[must_use]
125    pub const fn is_absolute(&self) -> bool {
126        self.absolute
127    }
128
129    /// The name's segments, outermost first.
130    #[must_use]
131    pub fn segments(&self) -> &[String] {
132        &self.segments
133    }
134
135    /// The innermost segment, if any.
136    #[must_use]
137    pub fn last(&self) -> Option<&String> {
138        self.segments.last()
139    }
140}
141
142impl fmt::Display for ScopedName {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        if self.absolute {
145            write!(f, "::")?;
146        }
147        for (index, segment) in self.segments.iter().enumerate() {
148            if index > 0 {
149                write!(f, "::")?;
150            }
151            write!(f, "{segment}")?;
152        }
153        Ok(())
154    }
155}
156
157/// Which ROS 2 interface file a name came from.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
159pub enum RosInterfaceKind {
160    /// `.msg`
161    Msg,
162    /// `.srv`
163    Srv,
164    /// `.action`
165    Action,
166}
167
168/// ROS 2 interface name: `package/kind/Name`.
169#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
170pub struct RosTypeName {
171    /// ROS package.
172    pub package: String,
173    /// Interface file kind.
174    pub kind: RosInterfaceKind,
175    /// Interface name.
176    pub name: String,
177}
178
179/// CORBA repository identifier, for example `IDL:Robot/Pose:1.0`.
180#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
181pub struct RepositoryId(pub String);
182
183/// XTypes type identifier in its opaque 16-byte form.
184///
185/// The model treats it as a lookup key; computing it is the runtime's
186/// concern, not this crate's.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
188pub struct TypeIdentifierKey(pub [u8; 16]);
189
190/// DDS-XML cross-reference target.
191#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
192pub struct XmlRefKey(pub String);
193
194/// Why resolution failed.
195///
196/// A reference must never sit in `Pending` after a failed attempt — the
197/// failure is recorded so it can be reported. Spec §2.
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum BreakCause {
200    /// Nothing in the model carries that name.
201    NotFound {
202        /// The name that was looked up.
203        what: PendingRef,
204    },
205    /// More than one node carries that name.
206    Ambiguous {
207        /// The name that was looked up.
208        what: PendingRef,
209        /// Every node that matched.
210        candidates: Vec<NodeId>,
211    },
212    /// The target exists but is the wrong kind of node.
213    KindMismatch {
214        /// Kind the reference required.
215        expected: &'static str,
216        /// Kind actually found.
217        found: &'static str,
218    },
219    /// Resolving would close a cycle the specification forbids, for
220    /// example a struct inheriting from itself.
221    IllegalCycle {
222        /// Node the cycle passes through.
223        through: NodeId,
224    },
225}
226
227#[cfg(test)]
228mod tests {
229    // Tests assert on Results directly; a failed expect IS the test failure.
230    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
231    use super::*;
232    use crate::arena::Arena;
233    use alloc::string::ToString;
234    use alloc::vec;
235
236    #[test]
237    fn resolved_yields_its_handle_and_the_others_do_not() {
238        let mut arena: Arena<u32> = Arena::new();
239        let id = arena.insert(1).expect("insert");
240
241        let resolved: Ref<u32> = Ref::Resolved(id);
242        let pending: Ref<u32> = Ref::Pending(PendingRef::ScopedName(ScopedName::absolute(vec![
243            "Robot".to_string(),
244            "Pose".to_string(),
245        ])));
246        let broken: Ref<u32> = Ref::Broken(BreakCause::NotFound {
247            what: PendingRef::RepositoryId(RepositoryId("IDL:Robot/Pose:1.0".to_string())),
248        });
249
250        assert_eq!(resolved.resolved(), Some(id));
251        assert_eq!(pending.resolved(), None);
252        assert_eq!(broken.resolved(), None);
253        assert!(resolved.is_resolved());
254        assert!(pending.is_pending());
255        assert!(broken.is_broken());
256    }
257
258    #[test]
259    fn pending_key_is_available_for_indexing_but_not_after_breaking() {
260        let key = PendingRef::RosTypeName(RosTypeName {
261            package: "geometry_msgs".to_string(),
262            kind: RosInterfaceKind::Msg,
263            name: "Pose".to_string(),
264        });
265        let pending: Ref<u32> = Ref::Pending(key.clone());
266        assert_eq!(pending.pending_key(), Some(&key));
267
268        let broken: Ref<u32> = Ref::Broken(BreakCause::NotFound { what: key });
269        assert_eq!(broken.pending_key(), None);
270        assert!(broken.break_cause().is_some());
271    }
272
273    #[test]
274    fn scoped_name_renders_idl_syntax() {
275        let name = ScopedName::absolute(vec!["Robot".to_string(), "Pose".to_string()]);
276        assert_eq!(name.to_string(), "::Robot::Pose");
277        let relative = ScopedName::relative(vec!["Pose".to_string()]);
278        assert_eq!(relative.to_string(), "Pose");
279        assert!(name.is_absolute());
280        assert!(!relative.is_absolute());
281    }
282
283    #[test]
284    fn scoped_names_order_segment_wise_for_deterministic_indices() {
285        let a = ScopedName::absolute(vec!["A".to_string()]);
286        let ab = ScopedName::absolute(vec!["A".to_string(), "B".to_string()]);
287        let b = ScopedName::absolute(vec!["B".to_string()]);
288        let mut all = vec![b.clone(), ab.clone(), a.clone()];
289        all.sort();
290        assert_eq!(all, vec![a, ab, b]);
291    }
292}