Skip to main content

yo_common/
addr.rs

1//! The 56 bit address: a 4 bit space and a 52 bit offset.
2//!
3//! This is the type that makes Y6 work. A vector's payload, a document's body,
4//! a graph node's properties and a Redis string are all reached through one
5//! index because the address says which world the offset lives in. None of the
6//! four models is layered on another, they just share an address width.
7//!
8//! 52 bits of offset is 4 PiB, which is past the point where a single file is
9//! the right answer, and 4 bits of space is 16 worlds against the 11 that exist.
10//! Both were sized once, here, and the sizes are checked by the tests below so
11//! that widening one later is a deliberate act.
12
13use core::fmt;
14
15/// Which world an [`Addr`] offset points into.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
17#[repr(u8)]
18#[non_exhaustive]
19pub enum Space {
20    /// The value is the 52 bits. Small integers and short strings live entirely
21    /// inside the index entry and cost zero dereferences. This is what Redis
22    /// calls `int` encoding and part of what it calls `embstr`.
23    Inline = 0,
24    /// A byte offset into the shard's arena segments (`05` section 3).
25    Arena = 1,
26    /// A byte offset into the shard's log region (`06` section 2).
27    Log = 2,
28    /// A hash, as an owner local element table.
29    Hash = 3,
30    /// A set, as an owner local element table or a dense member vector.
31    Set = 4,
32    /// A sorted set, as a counted B+ tree.
33    ZSet = 5,
34    /// A list, as a ring deque.
35    List = 6,
36    /// A stream, as a radix log.
37    Stream = 7,
38    /// A document, with its path indexes.
39    Doc = 8,
40    /// A vector partition set.
41    Vector = 9,
42    /// Graph adjacency.
43    Graph = 10,
44}
45
46impl Space {
47    /// Every space in numeric order.
48    pub const ALL: &'static [Space] = &[
49        Space::Inline,
50        Space::Arena,
51        Space::Log,
52        Space::Hash,
53        Space::Set,
54        Space::ZSet,
55        Space::List,
56        Space::Stream,
57        Space::Doc,
58        Space::Vector,
59        Space::Graph,
60    ];
61
62    /// The space for a raw 4 bit value, or `None` if nothing uses it yet.
63    #[inline]
64    pub const fn from_bits(bits: u8) -> Option<Space> {
65        match bits {
66            0 => Some(Space::Inline),
67            1 => Some(Space::Arena),
68            2 => Some(Space::Log),
69            3 => Some(Space::Hash),
70            4 => Some(Space::Set),
71            5 => Some(Space::ZSet),
72            6 => Some(Space::List),
73            7 => Some(Space::Stream),
74            8 => Some(Space::Doc),
75            9 => Some(Space::Vector),
76            10 => Some(Space::Graph),
77            _ => None,
78        }
79    }
80
81    /// The name that appears in `OBJECT ENCODING` style output and in errors.
82    #[inline]
83    pub const fn name(self) -> &'static str {
84        match self {
85            Space::Inline => "inline",
86            Space::Arena => "arena",
87            Space::Log => "log",
88            Space::Hash => "hash",
89            Space::Set => "set",
90            Space::ZSet => "zset",
91            Space::List => "list",
92            Space::Stream => "stream",
93            Space::Doc => "doc",
94            Space::Vector => "vector",
95            Space::Graph => "graph",
96        }
97    }
98}
99
100/// Bits of offset in an address.
101pub const OFFSET_BITS: u32 = 52;
102/// Bits of space in an address.
103pub const SPACE_BITS: u32 = 4;
104/// Total bits an address occupies in an index bucket.
105pub const ADDR_BITS: u32 = OFFSET_BITS + SPACE_BITS;
106
107/// The largest representable offset.
108pub const MAX_OFFSET: u64 = (1u64 << OFFSET_BITS) - 1;
109
110const OFFSET_MASK: u64 = MAX_OFFSET;
111
112/// A 56 bit address, held in the low 56 bits of a `u64`.
113///
114/// The zero address is reserved to mean "no entry" so that a cleared bucket
115/// needs no separate occupancy bit. That costs the `Inline` space its zero
116/// value, which is fine because an inline zero is stored as the integer 0 with
117/// a type tag rather than as a bare address.
118#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
119#[repr(transparent)]
120pub struct Addr(u64);
121
122impl Addr {
123    /// The absent address. A bucket entry holding this has no value.
124    pub const NONE: Addr = Addr(0);
125
126    /// Build an address.
127    ///
128    /// # Panics
129    ///
130    /// If `offset` does not fit in 52 bits. An offset that large means the file
131    /// has outgrown the address width, which is a design limit and not a
132    /// runtime condition, so it is a panic rather than an error value.
133    #[inline]
134    pub const fn new(space: Space, offset: u64) -> Addr {
135        assert!(offset <= MAX_OFFSET, "offset does not fit in 52 bits");
136        Addr(((space as u64) << OFFSET_BITS) | offset)
137    }
138
139    /// Build an address without checking the offset width.
140    ///
141    /// # Safety
142    ///
143    /// `offset` must be at or below [`MAX_OFFSET`]. Passing a wider value
144    /// silently corrupts the space bits, which sends a later read into the
145    /// wrong world.
146    #[inline]
147    pub const unsafe fn new_unchecked(space: Space, offset: u64) -> Addr {
148        Addr(((space as u64) << OFFSET_BITS) | offset)
149    }
150
151    /// The raw 56 bit value, as stored.
152    #[inline]
153    pub const fn to_bits(self) -> u64 {
154        self.0
155    }
156
157    /// Rebuild from a raw 56 bit value.
158    ///
159    /// Bits above 56 are dropped rather than trusted, because this value comes
160    /// off disk and a corrupt high byte should not become a wild pointer.
161    #[inline]
162    pub const fn from_bits(bits: u64) -> Addr {
163        Addr(bits & ((1u64 << ADDR_BITS) - 1))
164    }
165
166    /// Whether this address points at anything.
167    #[inline]
168    pub const fn is_none(self) -> bool {
169        self.0 == 0
170    }
171
172    /// Whether this address points at something.
173    #[inline]
174    pub const fn is_some(self) -> bool {
175        self.0 != 0
176    }
177
178    /// The offset part.
179    #[inline]
180    pub const fn offset(self) -> u64 {
181        self.0 & OFFSET_MASK
182    }
183
184    /// The raw space bits, before any check that they name a known space.
185    #[inline]
186    pub const fn space_bits(self) -> u8 {
187        (self.0 >> OFFSET_BITS) as u8
188    }
189
190    /// The space, or `None` if the bits name a space this build does not know.
191    ///
192    /// A file written by a newer release can carry a space we have never heard
193    /// of. That is a `VersionTooNew` condition for the caller to report, not a
194    /// panic, so this returns an option rather than unwrapping.
195    #[inline]
196    pub const fn space(self) -> Option<Space> {
197        Space::from_bits(self.space_bits())
198    }
199}
200
201impl fmt::Debug for Addr {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        if self.is_none() {
204            return f.write_str("Addr(none)");
205        }
206        match self.space() {
207            Some(s) => write!(f, "Addr({}+{:#x})", s.name(), self.offset()),
208            None => write!(f, "Addr(space{}+{:#x})", self.space_bits(), self.offset()),
209        }
210    }
211}
212
213/// Which shard owns a slot. Small because shard count is bounded by core count.
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
215#[repr(transparent)]
216pub struct ShardId(pub u16);
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn address_widths_are_what_the_bucket_assumes() {
224        // The index bucket packs seven addresses into 49 bytes and a link into
225        // 7 more. If this ever stops being 56, `05` section 2.1 is wrong.
226        assert_eq!(ADDR_BITS, 56);
227        assert_eq!(ADDR_BITS % 8, 0);
228    }
229
230    #[test]
231    fn round_trips_through_bits() {
232        for &space in Space::ALL {
233            for offset in [0u64, 1, 4096, MAX_OFFSET] {
234                let a = Addr::new(space, offset);
235                assert_eq!(a.offset(), offset);
236                assert_eq!(a.space(), Some(space));
237                assert_eq!(Addr::from_bits(a.to_bits()), a);
238            }
239        }
240    }
241
242    #[test]
243    fn zero_is_absent() {
244        assert!(Addr::NONE.is_none());
245        assert!(Addr::new(Space::Arena, 0).is_some());
246        assert!(Addr::new(Space::Inline, 1).is_some());
247    }
248
249    #[test]
250    fn unknown_space_reports_rather_than_panics() {
251        let a = Addr::from_bits(15u64 << OFFSET_BITS | 99);
252        assert_eq!(a.space(), None);
253        assert_eq!(a.space_bits(), 15);
254        assert_eq!(a.offset(), 99);
255    }
256
257    #[test]
258    fn high_byte_from_disk_is_dropped() {
259        let a = Addr::from_bits(0xFF00_0000_0000_0000 | 7);
260        assert_eq!(a.offset(), 7);
261        assert!(a.space_bits() <= 15);
262    }
263
264    #[test]
265    #[should_panic(expected = "52 bits")]
266    fn oversized_offset_panics() {
267        let _ = Addr::new(Space::Arena, MAX_OFFSET + 1);
268    }
269}