Skip to main content

strop_core/
id.rs

1//! Stable identities and typed coordinates (0014 wave 2).
2//!
3//! Two families:
4//! - **IDs**: generational arena keys (`DocumentId`, `ViewId`, …). A
5//!   closed document's id fails lookup instead of silently resolving to
6//!   whatever moved into its old vector slot.
7//! - **Coordinates**: newtypes so byte offsets, line indexes, and the
8//!   three column kinds (bytes / UTF-16 / display) can't mix silently.
9
10mod seed;
11pub use seed::{ArenaSeed, ArenaSeedError};
12use vstd::prelude::*;
13
14verus! {
15
16/// The slot-reuse decision, mathematically: a reused slot's occupant
17/// generation strictly advances — a generation that would wrap retires
18/// the slot for good, so a stale key can never alias a new occupant
19/// (0056 AR13).
20pub open spec fn generation_advances(current: int, next: int) -> bool {
21    next == current + 1
22}
23
24/// The next occupant generation for a reused slot, or retirement.
25/// `Arena::try_insert` consults this exact decision; a retired slot is
26/// dropped from the free list and never hands out an id again.
27pub fn next_generation(current: u32) -> (next: Option<u32>)
28    ensures
29        next.is_some() == (current < u32::MAX),
30        next.is_some() ==> generation_advances(current as int, next.unwrap() as int),
31        next.is_some() ==> next.unwrap() != current,
32{
33    if current == u32::MAX {
34        None
35    } else {
36        Some(current + 1)
37    }
38}
39
40/// The index-space decision: a fresh slot exists only while the slot
41/// count fits the u32 index space — a full index space refuses instead
42/// of truncating `slots.len()` onto a live slot (0056 AR13).
43/// `Arena::try_insert` consults this exact decision.
44pub fn index_for_len(len: usize) -> (index: Option<u32>)
45    ensures
46        index.is_some() == (len <= u32::MAX as usize),
47        index.is_some() ==> index.unwrap() as usize == len,
48{
49    if len > u32::MAX as usize {
50        None
51    } else {
52        Some(len as u32)
53    }
54}
55
56/// Reuse never aliases: the generation a slot hands out next differs
57/// from every generation it handed out before.
58proof fn reused_slot_never_aliases(current: int, next: int)
59    requires
60        generation_advances(current, next),
61    ensures
62        next != current,
63        next > current,
64{
65}
66
67}
68
69/// A generational-arena key: the index names the slot, the generation
70/// names the occupant. Stale keys fail lookup.
71#[derive(
72    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
73)]
74#[serde(bound = "")]
75pub struct Id<K> {
76    #[serde(rename = "slot", alias = "index")]
77    index: u32,
78    generation: u32,
79    #[serde(skip)]
80    _kind: std::marker::PhantomData<K>,
81}
82
83impl<K> Id<K> {
84    pub fn index(self) -> usize {
85        self.index as usize
86    }
87
88    pub fn generation(self) -> u32 {
89        self.generation
90    }
91}
92
93/// Marker kinds for the arena's identities.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
95pub struct DocumentKind;
96#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
97pub struct ViewKind;
98#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
99pub struct PaneKind;
100/// Registry keys for bound workspace contexts (0042 slice 2).
101#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
102pub struct WorkspaceKind;
103
104pub type DocumentId = Id<DocumentKind>;
105pub type ViewId = Id<ViewKind>;
106pub type PaneId = Id<PaneKind>;
107pub type WorkspaceId = Id<WorkspaceKind>;
108
109/// A minimal generational arena (house rule: 40 boring lines beat a
110/// dependency). Slots are reused; each reuse bumps the generation.
111/// Insertion is checked (0056 AR13): a slot whose generation would wrap
112/// is retired — never reused — so a stale key can never alias a new
113/// occupant, and a full index space refuses instead of truncating
114/// `slots.len()` onto a live slot.
115pub struct Arena<K, T> {
116    slots: Vec<Slot<T>>,
117    free: Vec<u32>,
118    _kind: std::marker::PhantomData<K>,
119}
120
121#[derive(Debug)]
122struct Slot<T> {
123    generation: u32,
124    value: Option<T>,
125}
126
127/// Identity allocation failed (0056 AR13): every slot is live or
128/// retired and the `u32` index space is full. Nothing was inserted and
129/// every existing id still resolves.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
131#[error("arena identity space exhausted")]
132pub struct ArenaExhausted;
133
134impl<K, T> Default for Arena<K, T> {
135    fn default() -> Self {
136        Self {
137            slots: Vec::new(),
138            free: Vec::new(),
139            _kind: std::marker::PhantomData,
140        }
141    }
142}
143
144impl<K, T> Arena<K, T> {
145    /// Checked insertion: reuses a free slot only when its generation
146    /// can advance without wrapping; retired (wrap-risk) slots are
147    /// dropped from the free list for good. Fails only when no slot is
148    /// reusable and the index space itself is full — the arena is left
149    /// unchanged, so an in-flight operation keeps every existing
150    /// document/edit.
151    pub fn try_insert(&mut self, value: T) -> Result<Id<K>, ArenaExhausted> {
152        while let Some(index) = self.free.pop() {
153            let slot = &mut self.slots[index as usize];
154            // The verified retirement rule (0057 VF18): a generation that
155            // cannot advance without wrapping retires the slot for good.
156            let Some(generation) = next_generation(slot.generation) else {
157                continue; // retire: this slot never hands out an id again
158            };
159            slot.generation = generation;
160            slot.value = Some(value);
161            return Ok(Id {
162                index,
163                generation,
164                _kind: std::marker::PhantomData,
165            });
166        }
167        // The verified index-space rule (0057 VF18): full space refuses
168        // instead of truncating `slots.len()` onto a live slot.
169        let Some(index) = index_for_len(self.slots.len()) else {
170            return Err(ArenaExhausted);
171        };
172        self.slots.push(Slot {
173            generation: 0,
174            value: Some(value),
175        });
176        Ok(Id {
177            index,
178            generation: 0,
179            _kind: std::marker::PhantomData,
180        })
181    }
182
183    /// How many more inserts this arena can serve: reusable free slots
184    /// plus the remaining index space. Multi-document operations
185    /// preflight against this instead of failing halfway through.
186    pub fn insert_capacity(&self) -> u64 {
187        let reusable = self
188            .free
189            .iter()
190            .filter(|&&index| self.slots[index as usize].generation < u32::MAX)
191            .count() as u64;
192        reusable + (u64::from(u32::MAX) - self.slots.len() as u64)
193    }
194
195    /// None for a stale id — never the wrong document.
196    pub fn get(&self, id: Id<K>) -> Option<&T> {
197        self.slots
198            .get(id.index as usize)
199            .filter(|s| s.generation == id.generation)
200            .and_then(|s| s.value.as_ref())
201    }
202
203    pub fn get_mut(&mut self, id: Id<K>) -> Option<&mut T> {
204        self.slots
205            .get_mut(id.index as usize)
206            .filter(|s| s.generation == id.generation)
207            .and_then(|s| s.value.as_mut())
208    }
209
210    /// Remove and return the value; stale ids get None.
211    pub fn remove(&mut self, id: Id<K>) -> Option<T> {
212        let slot = self.slots.get_mut(id.index as usize)?;
213        if slot.generation != id.generation {
214            return None;
215        }
216        let value = slot.value.take()?;
217        self.free.push(id.index);
218        Some(value)
219    }
220
221    pub fn iter(&self) -> impl Iterator<Item = (Id<K>, &T)> {
222        self.slots.iter().enumerate().filter_map(|(i, s)| {
223            s.value.as_ref().map(|v| {
224                (
225                    Id {
226                        index: i as u32,
227                        generation: s.generation,
228                        _kind: std::marker::PhantomData,
229                    },
230                    v,
231                )
232            })
233        })
234    }
235
236    /// Mutable values without copying keys or exposing vacant slots.
237    pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
238        self.slots.iter_mut().filter_map(|slot| slot.value.as_mut())
239    }
240
241    pub fn len(&self) -> usize {
242        self.slots.iter().filter(|s| s.value.is_some()).count()
243    }
244
245    /// Drop every live value (slot reuse still bumps generations).
246    pub fn clear(&mut self) {
247        let live: Vec<u32> = self
248            .slots
249            .iter()
250            .enumerate()
251            .filter(|(_, s)| s.value.is_some())
252            .map(|(i, _)| i as u32)
253            .collect();
254        for s in &mut self.slots {
255            s.value = None;
256        }
257        self.free.extend(live);
258    }
259    pub fn is_empty(&self) -> bool {
260        self.len() == 0
261    }
262}
263
264/// A coordinate newtype: the inner value is bytes (ByteOffset), lines
265/// (LineIndex), or columns in a named unit (ByteColumn/Utf16Column/
266/// DisplayColumn). Copy, ordered, hashable; arithmetic is explicit.
267macro_rules! coordinate {
268    ($name:ident, $unit:literal) => {
269        #[derive(
270            Debug,
271            Clone,
272            Copy,
273            PartialEq,
274            Eq,
275            PartialOrd,
276            Ord,
277            Hash,
278            Default,
279            serde::Serialize,
280            serde::Deserialize,
281        )]
282        #[repr(transparent)]
283        #[serde(transparent)]
284        pub struct $name(usize);
285
286        impl $name {
287            #[inline]
288            pub fn new(v: usize) -> Self {
289                Self(v)
290            }
291            /// Raw units (bytes / lines / columns per the type) — escape
292            /// hatch for arithmetic; naming the unit is the point.
293            #[inline]
294            pub fn get(self) -> usize {
295                self.0
296            }
297            #[inline]
298            pub fn saturating_sub(self, n: usize) -> Self {
299                Self(self.0.saturating_sub(n))
300            }
301        }
302
303        impl std::ops::AddAssign<usize> for $name {
304            #[inline]
305            fn add_assign(&mut self, n: usize) {
306                self.0 += n;
307            }
308        }
309        impl std::ops::SubAssign<usize> for $name {
310            #[inline]
311            fn sub_assign(&mut self, n: usize) {
312                self.0 -= n;
313            }
314        }
315        /// Raw-unit comparison: `offset > 0` reads naturally; the type
316        /// system still stops offset-vs-line mixes (the bug class).
317        impl PartialEq<usize> for $name {
318            #[inline]
319            fn eq(&self, other: &usize) -> bool {
320                self.0 == *other
321            }
322        }
323        impl PartialOrd<usize> for $name {
324            #[inline]
325            fn partial_cmp(&self, other: &usize) -> Option<std::cmp::Ordering> {
326                self.0.partial_cmp(other)
327            }
328        }
329        impl std::ops::Add<usize> for $name {
330            type Output = $name;
331            #[inline]
332            fn add(self, n: usize) -> $name {
333                $name(self.0 + n)
334            }
335        }
336        impl std::ops::Sub<usize> for $name {
337            type Output = $name;
338            #[inline]
339            fn sub(self, n: usize) -> $name {
340                $name(self.0 - n)
341            }
342        }
343        impl std::ops::Sub<$name> for $name {
344            type Output = usize; // a length
345            #[inline]
346            fn sub(self, other: $name) -> usize {
347                self.0 - other.0
348            }
349        }
350        impl std::fmt::Display for $name {
351            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352                write!(f, "{} {}", self.0, $unit)
353            }
354        }
355        impl From<$name> for usize {
356            #[inline]
357            fn from(v: $name) -> usize {
358                v.0
359            }
360        }
361        impl From<usize> for $name {
362            #[inline]
363            fn from(v: usize) -> $name {
364                $name(v)
365            }
366        }
367    };
368}
369
370coordinate!(ByteOffset, "B");
371coordinate!(LineIndex, "L");
372coordinate!(ByteColumn, "col:B");
373coordinate!(Utf16Column, "col:u16");
374coordinate!(DisplayColumn, "col:dsp");
375
376/// A document's content clock, not an LSP version, request ID or history node.
377#[derive(
378    Debug,
379    Clone,
380    Copy,
381    PartialEq,
382    Eq,
383    PartialOrd,
384    Ord,
385    Hash,
386    Default,
387    serde::Serialize,
388    serde::Deserialize,
389)]
390#[serde(transparent)]
391pub struct BufferRevision(u64);
392
393impl BufferRevision {
394    pub const fn new(value: u64) -> Self {
395        Self(value)
396    }
397    pub const fn get(self) -> u64 {
398        self.0
399    }
400    pub fn checked_next(self) -> Option<Self> {
401        self.0.checked_add(1).map(Self)
402    }
403}
404
405impl std::fmt::Display for BufferRevision {
406    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407        self.0.fmt(formatter)
408    }
409}
410
411impl From<u64> for BufferRevision {
412    fn from(value: u64) -> Self {
413        Self(value)
414    }
415}
416
417// NOTE: full newtype wrappers (ByteOffset(usize), Utf16Column(u32), …)
418// are the target; the pragmatic cutover is to name the domains first
419// (this module + the conversion functions) and tighten the Buffer API
420// per call-site cluster as waves 2–4 touch them. A big-bang usize→newtype
421// rewrite of every arithmetic site would be unreviewable.
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    #[test]
428    fn stale_ids_fail_lookup() {
429        let mut a: Arena<DocumentKind, String> = Arena::default();
430        let one = a.try_insert("one".into()).unwrap();
431        let two = a.try_insert("two".into()).unwrap();
432        assert_eq!(a.get(one).map(String::as_str), Some("one"));
433        a.remove(one);
434        assert_eq!(a.get(one), None, "removed");
435        let three = a.try_insert("three".into()).unwrap(); // reuses the slot
436        assert_eq!(a.get(one), None, "stale generation must not resolve");
437        assert_eq!(a.get(three).map(String::as_str), Some("three"));
438        assert_eq!(a.get(two).map(String::as_str), Some("two"));
439        assert_eq!(a.len(), 2);
440    }
441
442    /// Seeded at the generation boundary (0056 AR13): a slot at
443    /// u32::MAX-1 hands out one last id, then retires — the generation
444    /// never wraps onto a stale key.
445    #[test]
446    fn generation_wrap_retires_the_slot() {
447        let mut a: Arena<DocumentKind, String> = Arena::from_seed(ArenaSeed {
448            slots: vec![(u32::MAX - 1, None)],
449            free: vec![0],
450        })
451        .unwrap();
452        assert_eq!(a.insert_capacity(), u64::from(u32::MAX));
453        let last = a.try_insert("last".into()).unwrap();
454        assert_eq!((last.index(), last.generation()), (0, u32::MAX));
455        a.remove(last);
456        // The slot is now at u32::MAX: reuse would wrap onto `last`.
457        let fresh = a.try_insert("fresh".into()).unwrap();
458        assert_eq!((fresh.index(), fresh.generation()), (1, 0));
459        assert_eq!(a.get(last), None, "a wrapped generation would alias");
460        assert_eq!(a.get(fresh).map(String::as_str), Some("fresh"));
461        // Retirement is permanent: the freed fresh slot is reused, the
462        // retired slot stays dead, and capacity reflects both facts.
463        a.remove(fresh);
464        let again = a.try_insert("again".into()).unwrap();
465        assert_eq!((again.index(), again.generation()), (1, 1));
466        assert_eq!(a.insert_capacity(), u64::from(u32::MAX) - 2);
467    }
468
469    /// Retirement survives a seed round-trip: the rebuilt arena never
470    /// revives the dead slot (0056 AR13/R11).
471    #[test]
472    fn retired_slots_stay_dead_across_seeds() {
473        let mut a: Arena<DocumentKind, String> = Arena::from_seed(ArenaSeed {
474            slots: vec![(u32::MAX, None)],
475            free: vec![0],
476        })
477        .unwrap();
478        let live = a.try_insert("live".into()).unwrap();
479        assert_eq!(live.index(), 1, "the maxed slot was retired on reuse");
480        let seed = a.seed_with(|value| value.clone());
481        let mut rebuilt = Arena::from_seed(seed).unwrap();
482        let after = rebuilt.try_insert("after".into()).unwrap();
483        assert_eq!(after.index(), 2, "retirement is part of the seed");
484        assert_eq!(rebuilt.get(live).map(String::as_str), Some("live"));
485    }
486}