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 = size.to_string().parse().unwrap();
223        let role = Self::new_unchecked(
224            name,
225            Some(RoleParam::safe_static(u32::try_from(size).map_err(
226                |_| RoleValidationError::CountOverflow {
227                    count: u32::MAX,
228                    max: MAX_ROLE_COUNT,
229                },
230            )?)?),
231            None,
232            Some(size_token),
233        );
234        role.validate()?;
235        Ok(role)
236    }
237
238    /// Get the role name identifier.
239    #[must_use]
240    pub fn name(&self) -> &Ident {
241        &self.name
242    }
243
244    /// Get the role parameter if it exists.
245    #[must_use]
246    pub fn param(&self) -> Option<&RoleParam> {
247        self.param.as_ref()
248    }
249
250    /// Get the role index if it exists.
251    #[must_use]
252    pub fn index(&self) -> Option<&RoleIndex> {
253        self.index.as_ref()
254    }
255
256    /// Get the array size token if it exists.
257    #[must_use]
258    pub fn array_size(&self) -> Option<&TokenStream> {
259        self.array_size.as_ref()
260    }
261
262    /// Check if this role has an index
263    #[must_use]
264    pub fn is_indexed(&self) -> bool {
265        self.index.is_some()
266    }
267
268    /// Generate a Rust identifier for this role
269    #[must_use]
270    pub fn to_ident(&self) -> Ident {
271        self.name.clone()
272    }
273
274    /// Check if this role is parameterized (has either index or param)
275    #[must_use]
276    pub fn is_parameterized(&self) -> bool {
277        self.index.is_some() || self.param.is_some()
278    }
279
280    /// Check if this is a role array (declared with size like `Worker[N]`)
281    #[must_use]
282    pub fn is_array(&self) -> bool {
283        self.array_size.is_some() || matches!(self.param, Some(RoleParam::Static(_)))
284    }
285
286    /// Check if this role has dynamic parameterization (runtime count)
287    #[must_use]
288    pub fn is_dynamic(&self) -> bool {
289        matches!(self.param, Some(RoleParam::Runtime))
290    }
291
292    /// Check if this role has symbolic parameterization
293    #[must_use]
294    pub fn is_symbolic(&self) -> bool {
295        matches!(self.param, Some(RoleParam::Symbolic(_)))
296    }
297
298    /// Check if this role reference uses a wildcard index
299    #[must_use]
300    pub fn is_wildcard(&self) -> bool {
301        matches!(self.index, Some(RoleIndex::Wildcard))
302    }
303
304    /// Check if this role reference uses a range index
305    #[must_use]
306    pub fn is_range(&self) -> bool {
307        matches!(self.index, Some(RoleIndex::Range(_)))
308    }
309
310    /// Get the role parameter if it exists
311    #[must_use]
312    pub fn get_param(&self) -> Option<&RoleParam> {
313        self.param.as_ref()
314    }
315
316    /// Get the role index if it exists
317    #[must_use]
318    pub fn get_index(&self) -> Option<&RoleIndex> {
319        self.index.as_ref()
320    }
321
322    /// Get the static count for static parameterized roles
323    #[must_use]
324    pub fn get_static_count(&self) -> Option<u32> {
325        match &self.param {
326            Some(RoleParam::Static(count)) => Some(*count),
327            _ => None,
328        }
329    }
330
331    /// Get the symbolic name for symbolic parameterized roles
332    #[must_use]
333    pub fn get_symbolic_name(&self) -> Option<&str> {
334        match &self.param {
335            Some(RoleParam::Symbolic(name)) => Some(name),
336            _ => None,
337        }
338    }
339
340    /// Check if this role instance matches the given role family
341    ///
342    /// For parameterized roles, this checks if the base name matches,
343    /// ignoring specific indices. For example:
344    /// - `Worker[0]` matches `Worker[N]`
345    /// - `Worker[i]` matches `Worker[N]`
346    /// - `Worker[1]` matches `Worker[3]` (if `Worker[3]` is the array declaration)
347    /// - `Client` only matches `Client` (exact match for non-parameterized)
348    #[must_use]
349    pub fn matches_family(&self, family: &Role) -> bool {
350        // Names must match
351        if self.name != family.name {
352            return false;
353        }
354
355        // If the family is an array declaration (has array_size), any instance matches
356        if family.is_array() {
357            // Instance can have concrete index, symbolic param, or neither
358            return self.is_indexed() || self.param.is_some() || !self.is_array();
359        }
360
361        // If the family has a param (symbolic like `Worker[N]`), indexed instances match
362        if family.param.is_some() && (self.is_indexed() || self.param.is_some()) {
363            return true;
364        }
365
366        // Otherwise, require exact match
367        self == family
368    }
369
370    /// Validate this role for security and safety constraints
371    pub fn validate(&self) -> RoleValidationResult<()> {
372        // Validate role parameter
373        if let Some(param) = &self.param {
374            param.validate()?;
375        }
376
377        // Validate role index
378        if let Some(index) = &self.index {
379            index.validate()?;
380        }
381
382        // Validate consistency between param and index
383        if let (Some(param), Some(index)) = (&self.param, &self.index) {
384            param.validate_with_index(index)?;
385        }
386
387        Ok(())
388    }
389
390    /// Create a safe static role with overflow checking
391    pub fn safe_static(name: Ident, count: u32) -> RoleValidationResult<Self> {
392        if count > MAX_ROLE_COUNT {
393            return Err(RoleValidationError::CountOverflow {
394                count,
395                max: MAX_ROLE_COUNT,
396            });
397        }
398
399        Role::with_param(name, RoleParam::Static(count))
400    }
401
402    /// Create a safe indexed role with overflow checking
403    pub fn safe_indexed(name: Ident, index: u32) -> RoleValidationResult<Self> {
404        if index > MAX_ROLE_INDEX {
405            return Err(RoleValidationError::IndexOverflow {
406                index,
407                max: MAX_ROLE_INDEX,
408            });
409        }
410
411        Role::with_index(name, RoleIndex::Concrete(index))
412    }
413
414    /// Create a safe range role with overflow checking
415    pub fn safe_range(name: Ident, start: u32, end: u32) -> RoleValidationResult<Self> {
416        let range = RoleRange {
417            start: RangeExpr::Concrete(start),
418            end: RangeExpr::Concrete(end),
419        };
420        range.validate()?;
421
422        Role::with_index(name, RoleIndex::Range(range))
423    }
424}