Skip to main content

telltale_types/
de_bruijn.rs

1//! De Bruijn Index Representation for Session Types
2//!
3//! This module provides de Bruijn index representations for session types,
4//! enabling α-equivalence: types that differ only in variable names produce
5//! identical serializations.
6//!
7//! # De Bruijn Indices
8//!
9//! De Bruijn indices replace named variables with numeric indices representing
10//! binding depth. For example:
11//!
12//! ```text
13//! Named:       μx. A → B : x        μy. A → B : y
14//! De Bruijn:   μ. A → B : 0         μ. A → B : 0
15//!                    ↑ same canonical form ↑
16//! ```
17//!
18//! # Usage
19//!
20//! These types are **transient** - they exist only during serialization.
21//! The named representation (`GlobalType`, `LocalTypeR`) is preserved at runtime;
22//! de Bruijn conversion is performed only when computing content IDs.
23//!
24//! # Lean Correspondence
25//!
26//! This module corresponds to `lean/SessionTypes/LocalTypeDB/Core.lean`.
27
28use crate::{GlobalType, Label, LocalTypeR, ValType};
29use serde::{Deserialize, Serialize};
30
31/// Global type with de Bruijn indices (for serialization only).
32///
33/// This representation erases variable names, using numeric indices
34/// to reference binders. Two α-equivalent global types produce
35/// identical `GlobalTypeDB` values.
36///
37/// # Examples
38///
39/// ```
40/// use telltale_types::{GlobalType, Label, de_bruijn::GlobalTypeDB};
41///
42/// // These two types are α-equivalent
43/// let g1 = GlobalType::mu("x", GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")));
44/// let g2 = GlobalType::mu("y", GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")));
45///
46/// // They produce identical de Bruijn representations
47/// let db1 = GlobalTypeDB::from(&g1);
48/// let db2 = GlobalTypeDB::from(&g2);
49/// assert_eq!(db1, db2);
50/// ```
51#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
52pub enum GlobalTypeDB {
53    /// Protocol termination
54    End,
55    /// Communication: sender → receiver with choice of labeled continuations
56    Comm {
57        sender: String,
58        receiver: String,
59        branches: Vec<(Label, GlobalTypeDB)>,
60    },
61    /// Recursive type: μ.G (no variable name)
62    Rec(Box<GlobalTypeDB>),
63    /// Type variable: de Bruijn index (0 = innermost binder)
64    Var(usize),
65}
66
67impl GlobalTypeDB {
68    /// Convert a `GlobalType` to de Bruijn representation.
69    ///
70    /// Corresponds to Lean's `GlobalType.toDeBruijn`.
71    #[must_use]
72    pub fn from_global_type(g: &GlobalType) -> Self {
73        Self::from_global_type_with_env(g, &[])
74    }
75
76    pub(crate) fn from_global_type_with_env(g: &GlobalType, env: &[&str]) -> Self {
77        match g {
78            GlobalType::End => GlobalTypeDB::End,
79            GlobalType::Comm {
80                sender,
81                receiver,
82                branches,
83            } => GlobalTypeDB::Comm {
84                sender: sender.clone(),
85                receiver: receiver.clone(),
86                branches: branches
87                    .iter()
88                    .map(|(l, cont)| (l.clone(), Self::from_global_type_with_env(cont, env)))
89                    .collect(),
90            },
91            GlobalType::Mu { var, body } => {
92                // Push the variable name onto the environment stack
93                let mut new_env = vec![var.as_str()];
94                new_env.extend(env);
95                GlobalTypeDB::Rec(Box::new(Self::from_global_type_with_env(body, &new_env)))
96            }
97            GlobalType::Var(name) => {
98                // Find the index of this variable in the environment
99                let index = env.iter().position(|&v| v == name).unwrap_or(env.len()); // Unbound var gets index beyond env
100                GlobalTypeDB::Var(index)
101            }
102        }
103    }
104
105    /// Normalize branch ordering for deterministic serialization.
106    ///
107    /// Sorts branches by label name to ensure consistent ordering.
108    #[must_use]
109    pub fn normalize(&self) -> Self {
110        match self {
111            GlobalTypeDB::End => GlobalTypeDB::End,
112            GlobalTypeDB::Comm {
113                sender,
114                receiver,
115                branches,
116            } => {
117                let mut sorted_branches: Vec<_> = branches
118                    .iter()
119                    .map(|(l, cont)| (l.clone(), cont.normalize()))
120                    .collect();
121                sorted_branches.sort_by(|a, b| a.0.name.cmp(&b.0.name));
122                GlobalTypeDB::Comm {
123                    sender: sender.clone(),
124                    receiver: receiver.clone(),
125                    branches: sorted_branches,
126                }
127            }
128            GlobalTypeDB::Rec(body) => GlobalTypeDB::Rec(Box::new(body.normalize())),
129            GlobalTypeDB::Var(idx) => GlobalTypeDB::Var(*idx),
130        }
131    }
132}
133
134impl From<&GlobalType> for GlobalTypeDB {
135    fn from(g: &GlobalType) -> Self {
136        GlobalTypeDB::from_global_type(g)
137    }
138}
139
140/// Local type with de Bruijn indices (for serialization only).
141///
142/// This representation erases variable names, using numeric indices
143/// to reference binders. Two α-equivalent local types produce
144/// identical `LocalTypeRDB` values.
145///
146/// Unlike binder names, branch payload annotations (`Option<ValType>`) are
147/// semantic metadata and are preserved in this representation.
148///
149/// # Examples
150///
151/// ```
152/// use telltale_types::{LocalTypeR, Label, de_bruijn::LocalTypeRDB};
153///
154/// // These two types are α-equivalent
155/// let t1 = LocalTypeR::mu("x", LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")));
156/// let t2 = LocalTypeR::mu("y", LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("y")));
157///
158/// // They produce identical de Bruijn representations
159/// let db1 = LocalTypeRDB::from(&t1);
160/// let db2 = LocalTypeRDB::from(&t2);
161/// assert_eq!(db1, db2);
162/// ```
163#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
164pub enum LocalTypeRDB {
165    /// Protocol termination
166    End,
167    /// Internal choice: send to partner
168    Send {
169        partner: String,
170        branches: Vec<(Label, Option<ValType>, LocalTypeRDB)>,
171    },
172    /// External choice: receive from partner
173    Recv {
174        partner: String,
175        branches: Vec<(Label, Option<ValType>, LocalTypeRDB)>,
176    },
177    /// Recursive type: μ.T (no variable name)
178    Rec(Box<LocalTypeRDB>),
179    /// Type variable: de Bruijn index (0 = innermost binder)
180    Var(usize),
181}
182
183impl LocalTypeRDB {
184    /// Convert a `LocalTypeR` to de Bruijn representation.
185    ///
186    /// Corresponds to Lean's `LocalTypeR.toDeBruijn`.
187    #[must_use]
188    pub fn from_local_type(t: &LocalTypeR) -> Self {
189        Self::from_local_type_with_env(t, &[])
190    }
191
192    pub(crate) fn from_local_type_with_env(t: &LocalTypeR, env: &[&str]) -> Self {
193        match t {
194            LocalTypeR::End => LocalTypeRDB::End,
195            LocalTypeR::Send { partner, branches } => LocalTypeRDB::Send {
196                partner: partner.clone(),
197                branches: branches
198                    .iter()
199                    .map(|(l, vt, cont)| {
200                        (
201                            l.clone(),
202                            vt.clone(),
203                            Self::from_local_type_with_env(cont, env),
204                        )
205                    })
206                    .collect(),
207            },
208            LocalTypeR::Recv { partner, branches } => LocalTypeRDB::Recv {
209                partner: partner.clone(),
210                branches: branches
211                    .iter()
212                    .map(|(l, vt, cont)| {
213                        (
214                            l.clone(),
215                            vt.clone(),
216                            Self::from_local_type_with_env(cont, env),
217                        )
218                    })
219                    .collect(),
220            },
221            LocalTypeR::Mu { var, body } => {
222                // Push the variable name onto the environment stack
223                let mut new_env = vec![var.as_str()];
224                new_env.extend(env);
225                LocalTypeRDB::Rec(Box::new(Self::from_local_type_with_env(body, &new_env)))
226            }
227            LocalTypeR::Var(name) => {
228                // Find the index of this variable in the environment
229                let index = env.iter().position(|&v| v == name).unwrap_or(env.len()); // Unbound var gets index beyond env
230                LocalTypeRDB::Var(index)
231            }
232        }
233    }
234
235    /// Normalize branch ordering for deterministic serialization.
236    ///
237    /// Sorts branches by label name to ensure consistent ordering.
238    #[must_use]
239    pub fn normalize(&self) -> Self {
240        match self {
241            LocalTypeRDB::End => LocalTypeRDB::End,
242            LocalTypeRDB::Send { partner, branches } => {
243                let mut sorted_branches: Vec<_> = branches
244                    .iter()
245                    .map(|(l, vt, cont)| (l.clone(), vt.clone(), cont.normalize()))
246                    .collect();
247                sorted_branches.sort_by(|a, b| a.0.name.cmp(&b.0.name));
248                LocalTypeRDB::Send {
249                    partner: partner.clone(),
250                    branches: sorted_branches,
251                }
252            }
253            LocalTypeRDB::Recv { partner, branches } => {
254                let mut sorted_branches: Vec<_> = branches
255                    .iter()
256                    .map(|(l, vt, cont)| (l.clone(), vt.clone(), cont.normalize()))
257                    .collect();
258                sorted_branches.sort_by(|a, b| a.0.name.cmp(&b.0.name));
259                LocalTypeRDB::Recv {
260                    partner: partner.clone(),
261                    branches: sorted_branches,
262                }
263            }
264            LocalTypeRDB::Rec(body) => LocalTypeRDB::Rec(Box::new(body.normalize())),
265            LocalTypeRDB::Var(idx) => LocalTypeRDB::Var(*idx),
266        }
267    }
268}
269
270impl From<&LocalTypeR> for LocalTypeRDB {
271    fn from(t: &LocalTypeR) -> Self {
272        LocalTypeRDB::from_local_type(t)
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use assert_matches::assert_matches;
280
281    #[test]
282    fn test_alpha_equivalent_global_types() {
283        // μx. A → B : msg. x
284        let g1 = GlobalType::mu(
285            "x",
286            GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
287        );
288        // μy. A → B : msg. y (same structure, different variable name)
289        let g2 = GlobalType::mu(
290            "y",
291            GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
292        );
293
294        let db1 = GlobalTypeDB::from(&g1);
295        let db2 = GlobalTypeDB::from(&g2);
296
297        assert_eq!(db1, db2);
298    }
299
300    #[test]
301    fn test_alpha_equivalent_local_types() {
302        // μx. !B{msg.x}
303        let t1 = LocalTypeR::mu(
304            "x",
305            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
306        );
307        // μy. !B{msg.y} (same structure, different variable name)
308        let t2 = LocalTypeR::mu(
309            "y",
310            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("y")),
311        );
312
313        let db1 = LocalTypeRDB::from(&t1);
314        let db2 = LocalTypeRDB::from(&t2);
315
316        assert_eq!(db1, db2);
317    }
318
319    #[test]
320    fn test_nested_recursion() {
321        // μx. μy. A → B : msg. y
322        let g1 = GlobalType::mu(
323            "x",
324            GlobalType::mu(
325                "y",
326                GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
327            ),
328        );
329        // μa. μb. A → B : msg. b (same structure, different names)
330        let g2 = GlobalType::mu(
331            "a",
332            GlobalType::mu(
333                "b",
334                GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("b")),
335            ),
336        );
337
338        let db1 = GlobalTypeDB::from(&g1);
339        let db2 = GlobalTypeDB::from(&g2);
340
341        assert_eq!(db1, db2);
342
343        // Check the structure: innermost var should have index 0
344        assert_matches!(db1, GlobalTypeDB::Rec(outer) => {
345            assert_matches!(*outer, GlobalTypeDB::Rec(inner) => {
346                assert_matches!(*inner, GlobalTypeDB::Comm { ref branches, .. } => {
347                    assert_matches!(branches[0].1, GlobalTypeDB::Var(0));
348                });
349            });
350        });
351    }
352
353    #[test]
354    fn test_reference_to_outer_binder() {
355        // μx. μy. A → B : msg. x (reference to OUTER binder)
356        let g = GlobalType::mu(
357            "x",
358            GlobalType::mu(
359                "y",
360                GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
361            ),
362        );
363
364        let db = GlobalTypeDB::from(&g);
365
366        // The variable x should have index 1 (one level out)
367        assert_matches!(db, GlobalTypeDB::Rec(outer) => {
368            assert_matches!(*outer, GlobalTypeDB::Rec(inner) => {
369                assert_matches!(*inner, GlobalTypeDB::Comm { ref branches, .. } => {
370                    assert_matches!(branches[0].1, GlobalTypeDB::Var(1));
371                });
372            });
373        });
374    }
375
376    #[test]
377    fn test_different_structures_not_equal() {
378        // μx. A → B : msg. end
379        let g1 = GlobalType::mu(
380            "x",
381            GlobalType::send("A", "B", Label::new("msg"), GlobalType::End),
382        );
383        // μx. A → B : msg. x
384        let g2 = GlobalType::mu(
385            "x",
386            GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
387        );
388
389        let db1 = GlobalTypeDB::from(&g1);
390        let db2 = GlobalTypeDB::from(&g2);
391
392        assert_ne!(db1, db2);
393    }
394
395    #[test]
396    fn test_normalize_branch_order() {
397        // A → B : {b.end, a.end}
398        let g = GlobalType::comm(
399            "A",
400            "B",
401            vec![
402                (Label::new("b"), GlobalType::End),
403                (Label::new("a"), GlobalType::End),
404            ],
405        );
406
407        let db = GlobalTypeDB::from(&g).normalize();
408
409        assert_matches!(db, GlobalTypeDB::Comm { branches, .. } => {
410            assert_eq!(branches[0].0.name, "a");
411            assert_eq!(branches[1].0.name, "b");
412        });
413    }
414
415    #[test]
416    fn test_unbound_variable() {
417        // A → B : msg. t (t is unbound)
418        let g = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("t"));
419
420        let db = GlobalTypeDB::from(&g);
421
422        // Unbound variables get index 0 (beyond empty env)
423        assert_matches!(db, GlobalTypeDB::Comm { ref branches, .. } => {
424            assert_matches!(branches[0].1, GlobalTypeDB::Var(0));
425        });
426    }
427
428    #[test]
429    fn test_local_type_send_recv() {
430        // μx. !B{msg.?A{reply.x}}
431        let t = LocalTypeR::mu(
432            "x",
433            LocalTypeR::send(
434                "B",
435                Label::new("msg"),
436                LocalTypeR::recv("A", Label::new("reply"), LocalTypeR::var("x")),
437            ),
438        );
439
440        let db = LocalTypeRDB::from(&t);
441
442        assert_matches!(db, LocalTypeRDB::Rec(body) => {
443            assert_matches!(*body, LocalTypeRDB::Send { ref partner, ref branches } => {
444                assert_eq!(partner, "B");
445                assert_matches!(&branches[0].2, LocalTypeRDB::Recv { partner, branches: recv_branches } => {
446                    assert_eq!(partner, "A");
447                    assert_matches!(recv_branches[0].2, LocalTypeRDB::Var(0));
448                });
449            });
450        });
451    }
452}