Skip to main content

vyre_primitives/types/
linear_check.rs

1//! Linear-type discipline checker primitive (P-PRIM-14).
2//!
3//! Given a use-count and a declared discipline, the primitive answers
4//! "does the use-count satisfy the discipline?". The four disciplines
5//! (Linear, Affine, Relevant, Unrestricted) cover the standard
6//! substructural type system used by `vyre-foundation::validate` to
7//! reject ill-typed programs before lowering.
8//!
9//! Pure scalar primitive  -  no allocation, no IR dependency. The
10//! foundation's checker walks the program counting uses; this primitive
11//! is the single decision per buffer.
12
13/// Substructural-type discipline applied to one buffer or value.
14///
15/// Mirrors `vyre_foundation::ir::LinearType` but lives at the
16/// primitive layer so external crates (the type-checker pass, future
17/// effect-system frontends, external analyzer rule lowering) can refer to the
18/// discipline without pulling in the IR.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20#[non_exhaustive]
21pub enum LinearDiscipline {
22    /// Exactly one use. `uses == 1` is the only legal count.
23    Linear,
24    /// At most one use. `uses <= 1`.
25    Affine,
26    /// At least one use. `uses >= 1`.
27    Relevant,
28    /// No discipline. Any `uses` count is permitted.
29    Unrestricted,
30}
31
32impl LinearDiscipline {
33    /// Whether this discipline forbids dropping a buffer without using
34    /// it (`Linear` or `Relevant`).
35    #[must_use]
36    #[inline]
37    pub const fn forbids_drop(self) -> bool {
38        matches!(self, Self::Linear | Self::Relevant)
39    }
40
41    /// Whether this discipline forbids using a buffer more than once
42    /// (`Linear` or `Affine`).
43    #[must_use]
44    #[inline]
45    pub const fn forbids_reuse(self) -> bool {
46        matches!(self, Self::Linear | Self::Affine)
47    }
48}
49
50/// Why a use-count failed its discipline check.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum LinearTypeError {
53    /// Linear or Relevant discipline was declared but the count is 0.
54    Dropped {
55        /// The discipline that forbids drop.
56        discipline: LinearDiscipline,
57    },
58    /// Linear or Affine discipline was declared but the count is > 1.
59    Reused {
60        /// The discipline that forbids reuse.
61        discipline: LinearDiscipline,
62        /// The actual observed use count.
63        uses: u32,
64    },
65}
66
67/// Verify that `uses` satisfies the declared `discipline`.
68///
69/// Returns `Ok(())` when the count is acceptable, otherwise returns
70/// the precise discipline-violation reason. Pure const fn  -  single
71/// pattern match on the discipline plus one or two comparisons.
72///
73/// # Errors
74///
75/// Returns [`LinearTypeError::Dropped`] when a Linear/Relevant buffer
76/// has 0 uses, and [`LinearTypeError::Reused`] when a Linear/Affine
77/// buffer has > 1 uses.
78pub const fn check_linear_use(
79    discipline: LinearDiscipline,
80    uses: u32,
81) -> Result<(), LinearTypeError> {
82    match discipline {
83        LinearDiscipline::Linear => {
84            if uses == 0 {
85                Err(LinearTypeError::Dropped { discipline })
86            } else if uses > 1 {
87                Err(LinearTypeError::Reused { discipline, uses })
88            } else {
89                Ok(())
90            }
91        }
92        LinearDiscipline::Affine => {
93            if uses > 1 {
94                Err(LinearTypeError::Reused { discipline, uses })
95            } else {
96                Ok(())
97            }
98        }
99        LinearDiscipline::Relevant => {
100            if uses == 0 {
101                Err(LinearTypeError::Dropped { discipline })
102            } else {
103                Ok(())
104            }
105        }
106        LinearDiscipline::Unrestricted => Ok(()),
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn linear_one_use_is_ok() {
116        assert_eq!(check_linear_use(LinearDiscipline::Linear, 1), Ok(()));
117    }
118
119    #[test]
120    fn linear_zero_uses_is_dropped() {
121        let err = check_linear_use(LinearDiscipline::Linear, 0).unwrap_err();
122        assert_eq!(
123            err,
124            LinearTypeError::Dropped {
125                discipline: LinearDiscipline::Linear
126            }
127        );
128    }
129
130    #[test]
131    fn linear_two_uses_is_reused() {
132        let err = check_linear_use(LinearDiscipline::Linear, 2).unwrap_err();
133        assert_eq!(
134            err,
135            LinearTypeError::Reused {
136                discipline: LinearDiscipline::Linear,
137                uses: 2
138            }
139        );
140    }
141
142    #[test]
143    fn affine_zero_or_one_use_is_ok() {
144        assert_eq!(check_linear_use(LinearDiscipline::Affine, 0), Ok(()));
145        assert_eq!(check_linear_use(LinearDiscipline::Affine, 1), Ok(()));
146    }
147
148    #[test]
149    fn affine_multi_use_is_reused() {
150        let err = check_linear_use(LinearDiscipline::Affine, 3).unwrap_err();
151        assert!(matches!(err, LinearTypeError::Reused { uses: 3, .. }));
152    }
153
154    #[test]
155    fn relevant_zero_uses_is_dropped() {
156        let err = check_linear_use(LinearDiscipline::Relevant, 0).unwrap_err();
157        assert!(matches!(err, LinearTypeError::Dropped { .. }));
158    }
159
160    #[test]
161    fn relevant_any_nonzero_use_is_ok() {
162        assert_eq!(check_linear_use(LinearDiscipline::Relevant, 1), Ok(()));
163        assert_eq!(check_linear_use(LinearDiscipline::Relevant, 5), Ok(()));
164        assert_eq!(
165            check_linear_use(LinearDiscipline::Relevant, u32::MAX),
166            Ok(())
167        );
168    }
169
170    #[test]
171    fn unrestricted_accepts_anything() {
172        for uses in [0u32, 1, 2, 100, u32::MAX] {
173            assert_eq!(
174                check_linear_use(LinearDiscipline::Unrestricted, uses),
175                Ok(())
176            );
177        }
178    }
179
180    /// Closure-bar: forbids_drop / forbids_reuse helpers must agree
181    /// with the actual check_linear_use behavior at the boundary
182    /// counts (0 and 2).
183    #[test]
184    fn helpers_agree_with_checker_at_boundaries() {
185        for d in [
186            LinearDiscipline::Linear,
187            LinearDiscipline::Affine,
188            LinearDiscipline::Relevant,
189            LinearDiscipline::Unrestricted,
190        ] {
191            // forbids_drop ⇔ Err on uses=0
192            assert_eq!(
193                d.forbids_drop(),
194                check_linear_use(d, 0).is_err(),
195                "forbids_drop disagrees with checker at uses=0 for {:?}",
196                d
197            );
198            // forbids_reuse ⇔ Err on uses=2
199            assert_eq!(
200                d.forbids_reuse(),
201                check_linear_use(d, 2).is_err(),
202                "forbids_reuse disagrees with checker at uses=2 for {:?}",
203                d
204            );
205        }
206    }
207}