lattice_host/versioned.rs
1//! `Versioned<T>` — a tiny newtype wrapper that bumps a monotonic
2//! `u64` version on every `&mut` access via `DerefMut`.
3//!
4//! ## Why this exists
5//!
6//! Perf plan B.4: identity-preserving Arc publish for unchanged
7//! `RenderState` sub-states. Every `Editor::build_render_state`
8//! tick today freshly `Arc::new`s every sub-state struct even when
9//! the backing data hasn't moved. Most keystrokes don't touch
10//! `pane_tree` / `active_modes` / `buffer_locals` / `pane_highlights` /
11//! `lsp_progress`, so reusing the prior Arc when nothing changed
12//! drops both the outer allocation and the inner map / tree clones
13//! they would otherwise produce.
14//!
15//! The cache lookup needs a cheap-to-compare key. The naive option
16//! ("content-hash on every publish") makes the hash cost the
17//! rebuild cost — wash. The alternative ("dirty-flag set by every
18//! mutator") is bulletproof in spec but easy to forget at one of
19//! the ~30 mutation sites and fails silently when missed.
20//!
21//! `Versioned<T>` takes the third path: any code that obtains a
22//! `&mut` to the inner data goes through `DerefMut`, which
23//! increments the counter atomically with the access. There is no
24//! way to mutate through a `Versioned<T>` without bumping. Reads
25//! (`Deref`) don't bump. The cost is one `u64` add per `&mut`
26//! borrow — sub-nanosecond, dwarfed by whatever mutation follows.
27//!
28//! ## Trade-offs
29//!
30//! - Over-bumps on read-then-no-op-mutate (e.g. `.iter_mut()` that
31//! the caller never actually writes through) cause a spurious
32//! cache miss the next publish. Safe; just costs one extra
33//! rebuild of that sub-state. Bounded.
34//! - Field-assignment (`self.field = ...`) is not a `DerefMut` —
35//! it replaces the wrapper entirely. The `From<T>` /
36//! `Versioned::new` constructors zero the version, so the next
37//! `build_render_state` will rebuild correctly. Use [`Self::replace`]
38//! when you want to bump rather than reset.
39//! - Single-threaded only by design. The wrapper isn't atomic; it
40//! relies on `Editor` being mutated from one thread (the actor).
41//! If you need cross-thread mutation, you have other problems —
42//! talk to the actor instead of mutating Editor directly.
43
44use std::ops::{Deref, DerefMut};
45
46/// Newtype wrapper that bumps a `u64` version counter on every
47/// `DerefMut` access. See module docs for the rationale.
48#[derive(Debug, Default, Clone)]
49pub struct Versioned<T> {
50 inner: T,
51 version: u64,
52}
53
54impl<T> Versioned<T> {
55 /// Wrap a value at version 0.
56 pub fn new(inner: T) -> Self {
57 Self { inner, version: 0 }
58 }
59
60 /// Current version. Cache consumers compare against the version
61 /// stored alongside the cached output to decide whether to
62 /// reuse.
63 pub fn version(&self) -> u64 {
64 self.version
65 }
66
67 /// Replace the inner value AND bump the version (whereas a
68 /// plain `*v = Versioned::new(x)` would reset to 0). Use when
69 /// you need the swap to invalidate downstream caches.
70 pub fn replace(&mut self, value: T) -> T {
71 // Bump first so a panic in `mem::replace` doesn't leave us
72 // with stale (data, version) pairing.
73 self.version = self.version.wrapping_add(1);
74 std::mem::replace(&mut self.inner, value)
75 }
76
77 /// Unwrap to the underlying value, discarding the version.
78 pub fn into_inner(self) -> T {
79 self.inner
80 }
81}
82
83impl<T> Deref for Versioned<T> {
84 type Target = T;
85 fn deref(&self) -> &T {
86 &self.inner
87 }
88}
89
90impl<T> DerefMut for Versioned<T> {
91 fn deref_mut(&mut self) -> &mut T {
92 self.version = self.version.wrapping_add(1);
93 &mut self.inner
94 }
95}
96
97impl<T> From<T> for Versioned<T> {
98 fn from(inner: T) -> Self {
99 Self::new(inner)
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use std::collections::HashMap;
107
108 #[test]
109 fn new_starts_at_version_zero() {
110 let v: Versioned<i32> = Versioned::new(42);
111 assert_eq!(v.version(), 0);
112 assert_eq!(*v, 42);
113 }
114
115 #[test]
116 fn deref_does_not_bump() {
117 let v: Versioned<i32> = Versioned::new(7);
118 let _ = *v;
119 let _ = *v;
120 let _ = v.clone();
121 assert_eq!(v.version(), 0);
122 }
123
124 #[test]
125 fn deref_mut_bumps_each_access() {
126 let mut v: Versioned<i32> = Versioned::new(7);
127 *v += 1;
128 assert_eq!(v.version(), 1);
129 *v += 1;
130 assert_eq!(v.version(), 2);
131 }
132
133 #[test]
134 fn replace_bumps_and_returns_old() {
135 let mut v: Versioned<String> = Versioned::new("a".into());
136 let old = v.replace("b".into());
137 assert_eq!(old, "a");
138 assert_eq!(*v, "b");
139 assert_eq!(v.version(), 1);
140 }
141
142 #[test]
143 fn into_inner_discards_version() {
144 let mut v: Versioned<i32> = Versioned::new(1);
145 *v = 2;
146 assert_eq!(v.version(), 1);
147 let inner = v.into_inner();
148 assert_eq!(inner, 2);
149 }
150
151 #[test]
152 fn hashmap_mutator_through_autoref_bumps() {
153 // The whole point of the wrapper: an existing call like
154 // `self.field.insert(k, v)` autorefs `&mut self.field`,
155 // which fires DerefMut and bumps the version. No call-site
156 // change needed.
157 let mut v: Versioned<HashMap<u32, u32>> = Versioned::new(HashMap::new());
158 v.insert(1, 10);
159 assert_eq!(v.version(), 1);
160 v.insert(2, 20);
161 assert_eq!(v.version(), 2);
162 let _len = v.len();
163 let _val = v.get(&1);
164 assert_eq!(v.version(), 2);
165 }
166
167 #[test]
168 fn from_t_constructs_at_version_zero() {
169 let v: Versioned<i32> = 5.into();
170 assert_eq!(v.version(), 0);
171 assert_eq!(*v, 5);
172 }
173}