|
| 1 | +#![feature(adt_const_params)] |
| 2 | +#![feature(generic_const_exprs)] |
| 3 | +#![feature(const_fn_trait_bound)] |
| 4 | + |
| 5 | +use core::marker::PhantomData; |
| 6 | +use std::collections::HashMap; |
| 7 | + |
| 8 | +// const-evaluable equality for string slices |
| 9 | +pub const fn str_eq(lhs: &str, rhs: &str) -> bool { |
| 10 | + let lhs_bytes = lhs.as_bytes(); |
| 11 | + let rhs_bytes = rhs.as_bytes(); |
| 12 | + let mut i = 0; |
| 13 | + let bytes = if lhs_bytes.len() == rhs_bytes.len() { |
| 14 | + lhs_bytes.len() |
| 15 | + } else { |
| 16 | + return false; |
| 17 | + }; |
| 18 | + |
| 19 | + while i < bytes { |
| 20 | + if lhs_bytes[i] != rhs_bytes[i] { |
| 21 | + return false; |
| 22 | + } |
| 23 | + i += 1; |
| 24 | + } |
| 25 | + return true; |
| 26 | +} |
| 27 | + |
| 28 | +pub trait ContainsKey<const K: &'static str> {} |
| 29 | + |
| 30 | +// trait used to compare two types that have type-encoded lists of keys (in this cast static strings) |
| 31 | +pub trait KeySchema { |
| 32 | + const KEYS: &'static [&'static str]; |
| 33 | + const SIZE: usize; |
| 34 | +} |
| 35 | + |
| 36 | +pub struct KeyNil; |
| 37 | +impl KeySchema for KeyNil { |
| 38 | + const KEYS: &'static [&'static str] = &[]; |
| 39 | + const SIZE: usize = 0; |
| 40 | +} |
| 41 | + |
| 42 | +pub struct KeyCons<Tail, const KEY_ID: &'static str> |
| 43 | +where |
| 44 | + Tail: KeySchema, |
| 45 | +{ |
| 46 | + _tail: PhantomData<Tail>, |
| 47 | +} |
| 48 | + |
| 49 | +pub const fn compute_successor_size<T: KeySchema>() -> usize { |
| 50 | + T::SIZE + 1 |
| 51 | +} |
| 52 | + |
| 53 | +pub const fn construct_successor_array<Tail: KeySchema>( |
| 54 | + successor_key: &'static str, |
| 55 | +) -> [&'static str; compute_successor_size::<Tail>()] |
| 56 | +where |
| 57 | + [&'static str; compute_successor_size::<Tail>()]: Sized, |
| 58 | +{ |
| 59 | + let mut keys = [""; compute_successor_size::<Tail>()]; |
| 60 | + let tail_keys = Tail::KEYS; |
| 61 | + let mut i = 0; |
| 62 | + let old_array_size: usize = compute_successor_size::<Tail>() - 1; |
| 63 | + while i < old_array_size { |
| 64 | + keys[i] = tail_keys[i]; |
| 65 | + i += 1; |
| 66 | + } |
| 67 | + keys[old_array_size] = successor_key; |
| 68 | + keys |
| 69 | +} |
| 70 | + |
| 71 | +pub const fn is_equivalent_except<const K: &'static str>( |
| 72 | + with_k: &[&'static str], |
| 73 | + without_k: &[&'static str], |
| 74 | +) -> bool { |
| 75 | + let mut i = 0; |
| 76 | + while i < with_k.len() { |
| 77 | + if str_eq(with_k[i], K) { |
| 78 | + i += 1; |
| 79 | + continue; |
| 80 | + } |
| 81 | + let mut j = 0; |
| 82 | + let mut match_found = false; |
| 83 | + while j < without_k.len() { |
| 84 | + if str_eq(with_k[i], without_k[j]) { |
| 85 | + match_found = true; |
| 86 | + break; |
| 87 | + } |
| 88 | + j += 1; |
| 89 | + } |
| 90 | + if !match_found { |
| 91 | + return false; |
| 92 | + } |
| 93 | + i += 1; |
| 94 | + } |
| 95 | + return true; |
| 96 | +} |
| 97 | + |
| 98 | +// Outputs a usize in order to make the array invalid by underflowing |
| 99 | +// Alternatively this could use const_panic to produce good error messages |
| 100 | +pub const fn check_valid_subset<S1: KeySchema, S2: KeySchema, const K: &'static str>() -> usize |
| 101 | +where |
| 102 | + S1: ContainsKey<K>, |
| 103 | +{ |
| 104 | + let with_k: &[&'static str] = &S1::KEYS; |
| 105 | + let without_k: &[&'static str] = &S2::KEYS; |
| 106 | + |
| 107 | + if with_k.len() <= without_k.len() { |
| 108 | + // panic because S1 isn't bigger |
| 109 | + return (with_k.len() - 1) - without_k.len(); // panic using underflow |
| 110 | + } |
| 111 | + |
| 112 | + if !is_equivalent_except::<K>(with_k, without_k) { |
| 113 | + // panic because S2 doesn't have the rest of S1's elements |
| 114 | + return (without_k.len() - 1) - with_k.len(); // panic using underflow |
| 115 | + } |
| 116 | + |
| 117 | + return 1; |
| 118 | +} |
| 119 | + |
| 120 | +pub trait SubsetExcept<Parent: KeySchema, const K: &'static str>: KeySchema |
| 121 | +where |
| 122 | + [(); Parent::SIZE - Self::SIZE]: Sized, |
| 123 | + Parent: ContainsKey<K>, |
| 124 | +{ |
| 125 | +} |
| 126 | + |
| 127 | +impl<Schema, PossibleParent, const K: &'static str> SubsetExcept<PossibleParent, K> for Schema |
| 128 | +where |
| 129 | + Schema: KeySchema, |
| 130 | + PossibleParent: KeySchema, |
| 131 | + PossibleParent: ContainsKey<K>, |
| 132 | + [(); PossibleParent::SIZE - Schema::SIZE]: Sized, |
| 133 | + [(); check_valid_subset::<PossibleParent, Schema, K>()]: Sized, |
| 134 | +{ |
| 135 | +} |
| 136 | + |
| 137 | +impl<Tail, const KEY_ID: &'static str> KeySchema for KeyCons<Tail, KEY_ID> |
| 138 | +where |
| 139 | + Tail: KeySchema, |
| 140 | + [(); compute_successor_size::<Tail>()]: Sized, |
| 141 | +{ |
| 142 | + const KEYS: &'static [&'static str] = &construct_successor_array::<Tail>(KEY_ID); |
| 143 | + const SIZE: usize = compute_successor_size::<Tail>(); |
| 144 | +} |
| 145 | + |
| 146 | +// thanks to matt1992#5582 on the Rust Programming Language Community Discord for offering this strategy |
| 147 | +// a const expression calls a function, which provides a "proof" that a given type should always use a given implementation |
| 148 | +pub trait ContainsKeyHelper<const IS_EQUAL: bool, const K: &'static str> {} |
| 149 | + |
| 150 | +const fn contains_key_helper_helper<const KEY_ID: &'static str, const K: &'static str>() -> bool { |
| 151 | + str_eq(KEY_ID, K) |
| 152 | +} |
| 153 | +impl<Tail, const KEY_ID: &'static str, const K: &'static str> ContainsKey<K> |
| 154 | + for KeyCons<Tail, KEY_ID> |
| 155 | +where |
| 156 | + Tail: KeySchema, |
| 157 | + Self: ContainsKeyHelper<{ contains_key_helper_helper::<KEY_ID, K>() }, K>, |
| 158 | +{ |
| 159 | +} |
| 160 | + |
| 161 | +impl<Tail, const KEY_ID: &'static str, const K: &'static str> ContainsKeyHelper<false, K> |
| 162 | + for KeyCons<Tail, KEY_ID> |
| 163 | +where |
| 164 | + Tail: KeySchema + ContainsKey<K>, |
| 165 | +{ |
| 166 | +} |
| 167 | + |
| 168 | +impl<Tail, const KEY_ID: &'static str, const K: &'static str> ContainsKeyHelper<true, K> |
| 169 | + for KeyCons<Tail, KEY_ID> |
| 170 | +where |
| 171 | + Tail: KeySchema, |
| 172 | +{ |
| 173 | +} |
| 174 | + |
| 175 | +pub struct RestrictedStringMap<S: KeySchema> { |
| 176 | + inner: HashMap<&'static str, Option<String>>, |
| 177 | + // schemas should be 0-sized, but I use a phantom data here just to emphasize that there's no data dependency |
| 178 | + _schema: PhantomData<S>, |
| 179 | +} |
| 180 | +impl<S: KeySchema, const K: &'static str> ContainsKey<K> for RestrictedStringMap<S> where |
| 181 | + S: ContainsKey<K> |
| 182 | +{ |
| 183 | +} |
| 184 | +impl<S: KeySchema> RestrictedStringMap<S> |
| 185 | +where |
| 186 | + [(); compute_successor_size::<S>()]: Sized, |
| 187 | +{ |
| 188 | + pub fn empty_schema() -> RestrictedStringMap<KeyNil> { |
| 189 | + RestrictedStringMap::<_> { |
| 190 | + inner: HashMap::new(), |
| 191 | + // schemas should be 0-sized, but I use a phantom data here just to emphasize that there's no data dependency |
| 192 | + _schema: PhantomData::<_>, |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + pub fn new() -> Self { |
| 197 | + let mut hm: HashMap<&'static str, Option<String>> = HashMap::new(); |
| 198 | + |
| 199 | + for k in S::KEYS { |
| 200 | + hm.insert(*k, None); |
| 201 | + } |
| 202 | + |
| 203 | + hm.shrink_to_fit(); |
| 204 | + |
| 205 | + Self { |
| 206 | + inner: hm, |
| 207 | + _schema: PhantomData::<_>, |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + /// Adds a possible &'static str to the HashMap. |
| 212 | + /// This requires consuming the map since our type must change to reflect the new schema. |
| 213 | + pub fn add_key<const K: &'static str>(self) -> RestrictedStringMap<KeyCons<S, K>> |
| 214 | + where |
| 215 | + // Proof asserting that one size larger is still a valid schema |
| 216 | + // this should only be untrue if the number of keys exceeds usize::MAX |
| 217 | + [(); compute_successor_size::<S>()]: Sized, |
| 218 | + { |
| 219 | + let Self { mut inner, .. } = self; |
| 220 | + inner.insert(K, None); |
| 221 | + RestrictedStringMap::<_> { |
| 222 | + inner: inner, |
| 223 | + _schema: PhantomData::<_>, |
| 224 | + } |
| 225 | + } |
| 226 | + |
| 227 | + // I don't know of a way to remove the &'static str other than having the user provide their own new schema. |
| 228 | + // This is because I can't use a dependently typed function to construct a return type. |
| 229 | + // That's the only way I can think of to compute what the return type of such a function would look like without user input. |
| 230 | + // |
| 231 | + pub fn remove_key<NewSchema: KeySchema, const K: &'static str>( |
| 232 | + self, |
| 233 | + ) -> RestrictedStringMap<NewSchema> |
| 234 | + where |
| 235 | + Self: ContainsKey<K>, |
| 236 | + S: ContainsKey<K>, |
| 237 | + NewSchema: SubsetExcept<S, K>, |
| 238 | + [(); S::SIZE - NewSchema::SIZE]: Sized, |
| 239 | + { |
| 240 | + let Self { mut inner, .. } = self; |
| 241 | + inner.remove(&K); |
| 242 | + RestrictedStringMap::<_> { |
| 243 | + inner: inner, |
| 244 | + _schema: PhantomData::<_>, |
| 245 | + } |
| 246 | + } |
| 247 | +} |
| 248 | + |
| 249 | +fn foo() { |
| 250 | + let map: RestrictedStringMap<KeyNil> = RestrictedStringMap::<KeyNil>::empty_schema(); |
| 251 | + let mut map: RestrictedStringMap<KeyCons<KeyCons<KeyNil, "k1">, "k2">> = |
| 252 | + map.add_key::<"k1">().add_key::<"k2">(); |
| 253 | + let map: RestrictedStringMap<KeyCons<KeyNil, "k1">> = map.remove_key::<_, "k2">(); |
| 254 | +} |
| 255 | + |
| 256 | +fn main() {} |
0 commit comments