Skip to main content

protocol_droid/domain/ir/
features.rs

1//! `FeatureSet` — what an IR value *needs*, as distinct from what a session
2//! *has*.
3//!
4//! # Why this is not `FeatureProfile`
5//!
6//! [`FeatureProfile`](crate::domain::FeatureProfile) answers "what may this
7//! session do", and it is a consequence of negotiation: a version, two
8//! capability sets and a transport. A `FeatureSet` answers "what does this
9//! value require in order to be expressed", and it is a property of the value
10//! alone, with no session in sight.
11//!
12//! They share a bit encoding — both are subsets of [`Feature::ALL`] — so
13//! comparing them is a mask test, and that is the whole reason they stay
14//! separate types. Conflating them would make
15//! `session.profile.allows(..)` and `value.required_features().contains(..)`
16//! the same call, and a reader could not tell which question was being asked.
17//!
18//! The one place they meet is T08's lowering, where the check reads
19//! `required.is_subset_of(&profile_as_set)`. Keeping them distinct makes that
20//! line say what it is.
21
22use crate::domain::Feature;
23
24/// A set of features.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
26pub struct FeatureSet {
27    bits: u128,
28}
29
30impl FeatureSet {
31    /// No features. A value that needs nothing is one every revision can
32    /// express.
33    #[must_use]
34    pub const fn empty() -> Self {
35        Self { bits: 0 }
36    }
37
38    /// Adds a feature.
39    #[must_use]
40    pub fn with(self, feature: Feature) -> Self {
41        Self {
42            bits: self.bits | bit_of(feature),
43        }
44    }
45
46    /// Adds a feature in place.
47    pub fn insert(&mut self, feature: Feature) {
48        self.bits |= bit_of(feature);
49    }
50
51    /// Whether the feature is in the set.
52    #[must_use]
53    pub fn contains(self, feature: Feature) -> bool {
54        self.bits & bit_of(feature) != 0
55    }
56
57    /// Whether the set needs nothing.
58    #[must_use]
59    pub const fn is_empty(self) -> bool {
60        self.bits == 0
61    }
62
63    /// How many features the set holds.
64    #[must_use]
65    pub const fn len(self) -> u32 {
66        self.bits.count_ones()
67    }
68
69    /// The union of two sets.
70    ///
71    /// This is the workhorse: a result with five content blocks needs the union
72    /// of what each block needs, and a result with a content array plus
73    /// `structuredContent` needs the union of both.
74    #[must_use]
75    pub const fn union(self, other: Self) -> Self {
76        Self {
77            bits: self.bits | other.bits,
78        }
79    }
80
81    /// The union of everything in `features`.
82    #[must_use]
83    pub fn union_of(features: impl IntoIterator<Item = Feature>) -> Self {
84        features.into_iter().fold(Self::empty(), Self::with)
85    }
86
87    /// The features in `self` that are not in `other`.
88    #[must_use]
89    pub const fn difference(self, other: Self) -> Self {
90        Self {
91            bits: self.bits & !other.bits,
92        }
93    }
94
95    /// A set from the raw bit encoding.
96    ///
97    /// The encoding is shared with [`FeatureProfile`](crate::domain::FeatureProfile),
98    /// which is what lets `required.is_subset_of(&profile.as_set())` be one
99    /// mask test. This constructor is the only way to build a set from those
100    /// bits, and it exists so that sharing has exactly one door: a caller that
101    /// wanted a set from somewhere else would have to hand-assemble the same
102    /// `u128`, and the type would no longer be trustworthy as a mirror of a
103    /// profile.
104    #[must_use]
105    pub const fn from_bits(bits: u128) -> Self {
106        Self { bits }
107    }
108
109    /// The raw bit encoding, shared with [`FeatureProfile`](crate::domain::FeatureProfile).
110    #[must_use]
111    pub const fn bits(self) -> u128 {
112        self.bits
113    }
114
115    /// Whether every feature in `self` is also in `other`.
116    ///
117    /// This is the lowering gate: "can the session express this value".
118    #[must_use]
119    pub const fn is_subset_of(self, other: Self) -> bool {
120        self.bits & !other.bits == 0
121    }
122
123    /// The features in the set, in declaration order.
124    pub fn iter(self) -> impl Iterator<Item = Feature> {
125        Feature::ALL.into_iter().filter(move |f| self.contains(*f))
126    }
127}
128
129/// The bit a feature occupies, found by name.
130///
131/// The name is the identity rather than a hand-assigned number, so inserting a
132/// variant into [`Feature::ALL`] cannot leave a stale bit behind in a second
133/// place. Not `const`: `slice::get` is not const-callable on 1.98 and
134/// `indexing_slicing` is denied, so the two cannot both be satisfied — T05 hit
135/// this on `FeatureProfile` and made the same call.
136fn bit_of(feature: Feature) -> u128 {
137    let name = feature.as_str();
138    let mut index = 0;
139    while let Some(candidate) = Feature::ALL.get(index) {
140        if candidate.as_str() == name {
141            return 1u128 << index;
142        }
143        index += 1;
144    }
145    0
146}
147
148#[cfg(test)]
149mod tests {
150    use super::FeatureSet;
151    use crate::domain::Feature;
152
153    #[test]
154    fn an_empty_set_needs_nothing() {
155        let empty = FeatureSet::empty();
156        assert!(empty.is_empty());
157        assert_eq!(empty.len(), 0);
158        assert!(!empty.contains(Feature::Tools));
159    }
160
161    #[test]
162    fn a_single_feature_set_contains_exactly_that() {
163        let set = FeatureSet::empty().with(Feature::Tools);
164        assert!(set.contains(Feature::Tools));
165        assert!(!set.contains(Feature::Prompts));
166        assert_eq!(set.len(), 1);
167    }
168
169    #[test]
170    fn union_merges_and_deduplicates() {
171        let a = FeatureSet::empty()
172            .with(Feature::Tools)
173            .with(Feature::Prompts);
174        let b = FeatureSet::empty()
175            .with(Feature::Prompts)
176            .with(Feature::Resources);
177        let union = a.union(b);
178        assert_eq!(union.len(), 3);
179        for feature in [Feature::Tools, Feature::Prompts, Feature::Resources] {
180            assert!(union.contains(feature), "{feature}");
181        }
182    }
183
184    #[test]
185    fn union_of_iterates() {
186        let set = FeatureSet::union_of([Feature::Tools, Feature::Prompts]);
187        assert_eq!(set.len(), 2);
188        assert!(FeatureSet::union_of([]).is_empty());
189    }
190
191    /// The direction matters, and this is the direction the lowering gate uses:
192    /// "does the session have everything this value needs".
193    #[test]
194    fn subset_reflects_inclusion() {
195        let both = FeatureSet::empty()
196            .with(Feature::Tools)
197            .with(Feature::Prompts);
198        let one = FeatureSet::empty().with(Feature::Tools);
199
200        assert!(one.is_subset_of(both));
201        assert!(both.is_subset_of(both));
202        // An empty set is a subset of everything, including another empty one.
203        assert!(FeatureSet::empty().is_subset_of(both));
204        assert!(FeatureSet::empty().is_subset_of(one));
205        assert!(!both.is_subset_of(one));
206    }
207
208    #[test]
209    fn iter_yields_declaration_order() {
210        let set = FeatureSet::empty()
211            .with(Feature::Extensions)
212            .with(Feature::Tools);
213        let yielded: Vec<Feature> = set.iter().collect();
214        assert_eq!(yielded, vec![Feature::Tools, Feature::Extensions]);
215    }
216
217    #[test]
218    fn every_feature_gets_a_distinct_bit() {
219        let mut seen = Vec::new();
220        for feature in Feature::ALL {
221            let set = FeatureSet::empty().with(feature);
222            assert_eq!(set.len(), 1, "{feature} collided with another bit");
223            assert!(set.contains(feature), "{feature} is missing");
224            assert!(!seen.contains(&feature), "{feature} twice");
225            seen.push(feature);
226        }
227        assert_eq!(seen.len(), Feature::ALL.len());
228    }
229}