Skip to main content

telltale_language/ast/
role.rs

1//! Role definitions for choreographic protocols
2
3use proc_macro2::{Ident, TokenStream};
4
5#[path = "role_ops.rs"]
6mod ops;
7pub use ops::RoleBoundsChecker;
8
9/// Maximum allowed role count to prevent memory exhaustion
10pub const MAX_ROLE_COUNT: u32 = 10_000;
11
12/// Maximum allowed role index to prevent array bounds issues  
13pub const MAX_ROLE_INDEX: u32 = MAX_ROLE_COUNT - 1;
14
15/// Maximum allowed range size for role ranges
16pub const MAX_RANGE_COUNT: u32 = 1_000;
17
18/// Validation errors for dynamic role operations
19#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
20pub enum RoleValidationError {
21    #[error("Role count {count} exceeds maximum allowed {max}")]
22    CountOverflow { count: u32, max: u32 },
23
24    #[error("Role index {index} exceeds maximum allowed {max}")]
25    IndexOverflow { index: u32, max: u32 },
26
27    #[error("Range size {size} exceeds maximum allowed {max}")]
28    RangeSizeOverflow { size: u32, max: u32 },
29
30    #[error("Invalid range: start {start} >= end {end}")]
31    InvalidRange { start: u32, end: u32 },
32
33    #[error("Runtime role count must be bounded for safety")]
34    UnboundedRuntime,
35
36    #[error("Symbolic parameter '{param}' cannot be validated without runtime context")]
37    SymbolicValidation { param: String },
38}
39
40/// Result type for role validation operations
41pub type RoleValidationResult<T> = Result<T, RoleValidationError>;
42
43/// Role parameter expression for dynamic role counts
44#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub enum RoleParam {
46    /// Static count: `Worker[3]`
47    Static(u32),
48    /// Symbolic count: `Worker[N]`
49    Symbolic(String),
50    /// Runtime determined: `Worker[*]`
51    Runtime,
52}
53
54/// Role index expression for role references
55#[derive(Debug, Clone, PartialEq, Eq, Hash)]
56pub enum RoleIndex {
57    /// Concrete index: `Worker[0]`
58    Concrete(u32),
59    /// Symbolic index: `Worker[i]`
60    Symbolic(String),
61    /// Wildcard: `Worker[*]` - all instances
62    Wildcard,
63    /// Range: `Worker[0..3]`
64    Range(RoleRange),
65}
66
67/// Role range specification for role references
68#[derive(Debug, Clone, PartialEq, Eq, Hash)]
69pub struct RoleRange {
70    /// Start of range (inclusive)
71    pub start: RangeExpr,
72    /// End of range (exclusive)
73    pub end: RangeExpr,
74}
75
76/// Range expression (can be concrete or symbolic)
77#[derive(Debug, Clone, PartialEq, Eq, Hash)]
78pub enum RangeExpr {
79    /// Concrete value: 0, 3
80    Concrete(u32),
81    /// Symbolic value: i, N
82    Symbolic(String),
83}
84
85/// A role (participant) in the choreography
86///
87/// Roles represent the different participants in a distributed protocol.
88/// They can be simple (e.g., `Client`, `Server`) or parameterized
89/// (e.g., `Worker[0]`, `Worker[N]` where the parameter can be a constant or variable).
90///
91/// # Examples
92///
93/// ```text
94/// use quote::format_ident;
95/// use telltale_runtime::{Role, RoleParam};
96///
97/// // Simple role
98/// let client = Role::new(format_ident!("Client")).unwrap();
99///
100/// // Static parameterized role
101/// let worker = Role::with_param(format_ident!("Worker"), RoleParam::Static(3)).unwrap();
102///
103/// // Dynamic role
104/// let dynamic_worker = Role::with_param(format_ident!("Worker"), RoleParam::Runtime).unwrap();
105/// ```
106#[derive(Debug, Clone)]
107pub struct Role {
108    /// The name identifier of the role
109    name: Ident,
110    /// Optional parameter for role count/size
111    param: Option<RoleParam>,
112    /// Optional index for role references
113    index: Option<RoleIndex>,
114    /// Array size for code generation (e.g., for `Worker[N]`)
115    array_size: Option<TokenStream>,
116}
117
118// Manual implementations for PartialEq, Eq, and Hash
119impl PartialEq for Role {
120    fn eq(&self, other: &Self) -> bool {
121        self.name == other.name
122            && self.param == other.param
123            && self.index == other.index
124            && self
125                .array_size
126                .as_ref()
127                .map(std::string::ToString::to_string)
128                == other
129                    .array_size
130                    .as_ref()
131                    .map(std::string::ToString::to_string)
132    }
133}
134
135impl Eq for Role {}
136
137impl std::hash::Hash for Role {
138    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
139        self.name.hash(state);
140        self.param.hash(state);
141        self.index.hash(state);
142        if let Some(size) = &self.array_size {
143            size.to_string().hash(state);
144        }
145    }
146}
147
148impl Role {
149    fn new_unchecked(
150        name: Ident,
151        param: Option<RoleParam>,
152        index: Option<RoleIndex>,
153        array_size: Option<TokenStream>,
154    ) -> Self {
155        Role {
156            name,
157            param,
158            index,
159            array_size,
160        }
161    }
162
163    /// Create a new simple role with the given name
164    pub fn new(name: Ident) -> RoleValidationResult<Self> {
165        let role = Self::new_unchecked(name, None, None, None);
166        role.validate()?;
167        Ok(role)
168    }
169
170    /// Create a role with a parameter (e.g., `Worker[3]`, `Worker[N]`, `Worker[*]`)
171    pub fn with_param(name: Ident, param: RoleParam) -> RoleValidationResult<Self> {
172        let role = Self::new_unchecked(name, Some(param), None, None);
173        role.validate()?;
174        Ok(role)
175    }
176
177    /// Create a role reference with an index (e.g., `Worker[0]`, `Worker[i]`, `Worker[*]`)
178    pub fn with_index(name: Ident, index: RoleIndex) -> RoleValidationResult<Self> {
179        let role = Self::new_unchecked(name, None, Some(index), None);
180        role.validate()?;
181        Ok(role)
182    }
183
184    /// Create a role reference with both param and index
185    pub fn with_param_and_index(
186        name: Ident,
187        param: RoleParam,
188        index: RoleIndex,
189    ) -> RoleValidationResult<Self> {
190        let role = Self::new_unchecked(name, Some(param), Some(index), None);
191        role.validate()?;
192        Ok(role)
193    }
194
195    /// Create a new indexed role (e.g., Worker with index 0)
196    pub fn indexed(name: Ident, index: usize) -> RoleValidationResult<Self> {
197        let role_index = RoleIndex::safe_concrete(u32::try_from(index).map_err(|_| {
198            RoleValidationError::IndexOverflow {
199                index: u32::MAX,
200                max: MAX_ROLE_INDEX,
201            }
202        })?)?;
203        let role = Self::new_unchecked(name, None, Some(role_index), None);
204        role.validate()?;
205        Ok(role)
206    }
207
208    /// Create a parameterized role with symbolic parameter (e.g., `Worker[N]`)
209    pub fn parameterized(name: Ident, param: TokenStream) -> RoleValidationResult<Self> {
210        let role = Self::new_unchecked(
211            name,
212            Some(RoleParam::Symbolic(param.to_string())),
213            None,
214            Some(param),
215        );
216        role.validate()?;
217        Ok(role)
218    }
219
220    /// Create a role array with a concrete size (e.g., `Worker[3]`)
221    pub fn array(name: Ident, size: usize) -> RoleValidationResult<Self> {
222        let size_token = TokenStream::from(proc_macro2::TokenTree::Literal(
223            proc_macro2::Literal::usize_unsuffixed(size),
224        ));
225        let role = Self::new_unchecked(
226            name,
227            Some(RoleParam::safe_static(u32::try_from(size).map_err(
228                |_| RoleValidationError::CountOverflow {
229                    count: u32::MAX,
230                    max: MAX_ROLE_COUNT,
231                },
232            )?)?),
233            None,
234            Some(size_token),
235        );
236        role.validate()?;
237        Ok(role)
238    }
239
240    /// Get the role name identifier.
241    #[must_use]
242    pub fn name(&self) -> &Ident {
243        &self.name
244    }
245
246    /// Get the role parameter if it exists.
247    #[must_use]
248    pub fn param(&self) -> Option<&RoleParam> {
249        self.param.as_ref()
250    }
251
252    /// Get the role index if it exists.
253    #[must_use]
254    pub fn index(&self) -> Option<&RoleIndex> {
255        self.index.as_ref()
256    }
257
258    /// Get the array size token if it exists.
259    #[must_use]
260    pub fn array_size(&self) -> Option<&TokenStream> {
261        self.array_size.as_ref()
262    }
263
264    /// Check if this role has an index
265    #[must_use]
266    pub fn is_indexed(&self) -> bool {
267        self.index.is_some()
268    }
269
270    /// Generate a Rust identifier for this role
271    #[must_use]
272    pub fn to_ident(&self) -> Ident {
273        self.name.clone()
274    }
275
276    /// Check if this role is parameterized (has either index or param)
277    #[must_use]
278    pub fn is_parameterized(&self) -> bool {
279        self.index.is_some() || self.param.is_some()
280    }
281
282    /// Check if this is a role array (declared with size like `Worker[N]`)
283    #[must_use]
284    pub fn is_array(&self) -> bool {
285        self.array_size.is_some() || matches!(self.param, Some(RoleParam::Static(_)))
286    }
287
288    /// Check if this role has dynamic parameterization (runtime count)
289    #[must_use]
290    pub fn is_dynamic(&self) -> bool {
291        matches!(self.param, Some(RoleParam::Runtime))
292    }
293
294    /// Check if this role has symbolic parameterization
295    #[must_use]
296    pub fn is_symbolic(&self) -> bool {
297        matches!(self.param, Some(RoleParam::Symbolic(_)))
298    }
299
300    /// Check if this role reference uses a wildcard index
301    #[must_use]
302    pub fn is_wildcard(&self) -> bool {
303        matches!(self.index, Some(RoleIndex::Wildcard))
304    }
305
306    /// Check if this role reference uses a range index
307    #[must_use]
308    pub fn is_range(&self) -> bool {
309        matches!(self.index, Some(RoleIndex::Range(_)))
310    }
311
312    /// Get the role parameter if it exists
313    #[must_use]
314    pub fn get_param(&self) -> Option<&RoleParam> {
315        self.param.as_ref()
316    }
317
318    /// Get the role index if it exists
319    #[must_use]
320    pub fn get_index(&self) -> Option<&RoleIndex> {
321        self.index.as_ref()
322    }
323
324    /// Get the static count for static parameterized roles
325    #[must_use]
326    pub fn get_static_count(&self) -> Option<u32> {
327        match &self.param {
328            Some(RoleParam::Static(count)) => Some(*count),
329            _ => None,
330        }
331    }
332
333    /// Get the symbolic name for symbolic parameterized roles
334    #[must_use]
335    pub fn get_symbolic_name(&self) -> Option<&str> {
336        match &self.param {
337            Some(RoleParam::Symbolic(name)) => Some(name),
338            _ => None,
339        }
340    }
341
342    /// Check if this role instance matches the given role family
343    ///
344    /// For parameterized roles, this checks if the base name matches,
345    /// ignoring specific indices. For example:
346    /// - `Worker[0]` matches `Worker[N]`
347    /// - `Worker[i]` matches `Worker[N]`
348    /// - `Worker[1]` matches `Worker[3]` (if `Worker[3]` is the array declaration)
349    /// - `Client` only matches `Client` (exact match for non-parameterized)
350    #[must_use]
351    pub fn matches_family(&self, family: &Role) -> bool {
352        // Names must match
353        if self.name != family.name {
354            return false;
355        }
356
357        // If the family is an array declaration (has array_size), any instance matches
358        if family.is_array() {
359            // Instance can have concrete index, symbolic param, or neither
360            return self.is_indexed() || self.param.is_some() || !self.is_array();
361        }
362
363        // If the family has a param (symbolic like `Worker[N]`), indexed instances match
364        if family.param.is_some() && (self.is_indexed() || self.param.is_some()) {
365            return true;
366        }
367
368        // Otherwise, require exact match
369        self == family
370    }
371
372    /// Validate this role for security and safety constraints
373    pub fn validate(&self) -> RoleValidationResult<()> {
374        // Validate role parameter
375        if let Some(param) = &self.param {
376            param.validate()?;
377        }
378
379        // Validate role index
380        if let Some(index) = &self.index {
381            index.validate()?;
382        }
383
384        // Validate consistency between param and index
385        if let (Some(param), Some(index)) = (&self.param, &self.index) {
386            param.validate_with_index(index)?;
387        }
388
389        Ok(())
390    }
391
392    /// Create a safe static role with overflow checking
393    pub fn safe_static(name: Ident, count: u32) -> RoleValidationResult<Self> {
394        if count > MAX_ROLE_COUNT {
395            return Err(RoleValidationError::CountOverflow {
396                count,
397                max: MAX_ROLE_COUNT,
398            });
399        }
400
401        Role::with_param(name, RoleParam::Static(count))
402    }
403
404    /// Create a safe indexed role with overflow checking
405    pub fn safe_indexed(name: Ident, index: u32) -> RoleValidationResult<Self> {
406        if index > MAX_ROLE_INDEX {
407            return Err(RoleValidationError::IndexOverflow {
408                index,
409                max: MAX_ROLE_INDEX,
410            });
411        }
412
413        Role::with_index(name, RoleIndex::Concrete(index))
414    }
415
416    /// Create a safe range role with overflow checking
417    pub fn safe_range(name: Ident, start: u32, end: u32) -> RoleValidationResult<Self> {
418        let range = RoleRange {
419            start: RangeExpr::Concrete(start),
420            end: RangeExpr::Concrete(end),
421        };
422        range.validate()?;
423
424        Role::with_index(name, RoleIndex::Range(range))
425    }
426}