Skip to main content

chisel/
handle.rs

1// src/handle.rs — public newtypes for the Chisel API surface.
2//
3// Role in system: the public boundary's value types. `Handle` and `Tag` wrap
4// the engine's raw `u64`/`u32` ids so the public API is not primitive-obsessed
5// — `delete_tagged(Handle, Tag)` cannot be called with its arguments
6// transposed, and "no tag" is the absence of a `Tag` (`Option<Tag>`), never the
7// in-band sentinel `0`. These types live ABOVE the engine: nothing in
8// transaction.rs / handle_table.rs / membership_index.rs knows about them;
9// lib.rs converts at the edge via `.get()` / `From`. The on-disk format and the
10// radix-tree key arithmetic stay raw integers (ISSUES.md I120/I126).
11//
12// Ergonomics decision (ISSUES.md I120): the newtypes carry `PartialEq` against
13// their raw primitive and `Display`, so existing call sites that compare or
14// print ids keep compiling. Distinctness — not opacity of comparison — is what
15// blocks transposition.
16
17use std::fmt;
18use std::num::NonZeroU32;
19
20/// A stable, opaque chunk handle. Newtype over the engine's `u64` id.
21///
22/// `#[repr(transparent)]` is load-bearing, not decoration: the bench adapter
23/// reinterprets a `&[u64]` slice as `&[Handle]` without copying, which is sound
24/// only because `Handle` has identical layout to `u64`.
25#[repr(transparent)]
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
27pub struct Handle(u64);
28
29// Pin the layout the bench adapter's `&[u64]` -> `&[Handle]` transmute depends
30// on (bench/src/chisel_engine.rs). With this, dropping `#[repr(transparent)]` or
31// adding a field fails THIS crate's build with a clear message, instead of
32// turning the cross-crate transmute into silent UB the bench can't detect
33// (review 2026-06-22). const assertions are evaluated at compile time.
34const _: () = {
35    assert!(
36        core::mem::size_of::<Handle>() == core::mem::size_of::<u64>(),
37        "Handle must stay layout-identical to u64 (bench transmute relies on repr(transparent))"
38    );
39    assert!(
40        core::mem::align_of::<Handle>() == core::mem::align_of::<u64>(),
41        "Handle must stay layout-identical to u64 (bench transmute relies on repr(transparent))"
42    );
43};
44
45impl Handle {
46    /// The underlying raw id. Handles are minted from `1` (the engine reserves
47    /// `0` as the "no handle" sentinel); this carrier type does not itself
48    /// enforce non-zero.
49    #[must_use]
50    pub const fn get(self) -> u64 {
51        self.0
52    }
53}
54
55impl From<u64> for Handle {
56    fn from(v: u64) -> Self {
57        Handle(v)
58    }
59}
60
61impl From<Handle> for u64 {
62    fn from(h: Handle) -> Self {
63        h.0
64    }
65}
66
67// PartialEq against the raw primitive in both directions so `assert_eq!(h, 1)`
68// and `assert_eq!(1, h)` both compile. The integer literal infers as `u64`
69// because that is the only integer type `Handle` compares against.
70impl PartialEq<u64> for Handle {
71    fn eq(&self, other: &u64) -> bool {
72        self.0 == *other
73    }
74}
75
76impl PartialEq<Handle> for u64 {
77    fn eq(&self, other: &Handle) -> bool {
78        *self == other.0
79    }
80}
81
82impl fmt::Display for Handle {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "{}", self.0)
85    }
86}
87
88/// A non-zero chunk tag. "No tag" is the ABSENCE of a `Tag` (`Option<Tag>`) —
89/// `Tag(0)` is unconstructable, which makes the untagged sentinel expressible
90/// in the type (ISSUES.md I126). The engine still stores the tag as a `u32`
91/// with `0` meaning untagged; the `0 <-> None` mapping happens at the lib.rs
92/// boundary.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
94pub struct Tag(NonZeroU32);
95
96impl Tag {
97    /// Construct a `Tag`, returning `None` if `v == 0`. Mirrors
98    /// `NonZeroU32::new`.
99    #[must_use]
100    pub const fn new(v: u32) -> Option<Tag> {
101        match NonZeroU32::new(v) {
102            Some(nz) => Some(Tag(nz)),
103            None => None,
104        }
105    }
106
107    /// The underlying raw tag value (always `>= 1`).
108    #[must_use]
109    pub const fn get(self) -> u32 {
110        self.0.get()
111    }
112}
113
114/// Error from `Tag::try_from(0)`. Zero-size; deliberately NOT a `ChiselError`
115/// variant — tag construction is fallible only at the API boundary, never
116/// inside the engine.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub struct ZeroTagError;
119
120impl fmt::Display for ZeroTagError {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        write!(f, "tag must be non-zero (0 is the untagged sentinel)")
123    }
124}
125
126impl std::error::Error for ZeroTagError {}
127
128impl TryFrom<u32> for Tag {
129    type Error = ZeroTagError;
130    fn try_from(v: u32) -> Result<Tag, ZeroTagError> {
131        Tag::new(v).ok_or(ZeroTagError)
132    }
133}
134
135impl From<Tag> for u32 {
136    fn from(t: Tag) -> Self {
137        t.0.get()
138    }
139}
140
141impl PartialEq<u32> for Tag {
142    fn eq(&self, other: &u32) -> bool {
143        self.get() == *other
144    }
145}
146
147impl PartialEq<Tag> for u32 {
148    fn eq(&self, other: &Tag) -> bool {
149        *self == other.get()
150    }
151}
152
153impl fmt::Display for Tag {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        write!(f, "{}", self.get())
156    }
157}
158
159/// Progress report from `Chisel::delete_with_tag`. `deleted` lists the handles
160/// removed in this pass (the engine deletes their chunks); `complete` is `true`
161/// when the tag has no remaining members. Produced only on the success path —
162/// a mid-pass error returns `Err` and no progress (the per-delete state stays
163/// consistent; only the reporting is lost).
164//
165// Lives here, above the engine, because its `deleted` field is `Vec<Handle>` —
166// a public-surface type. The engine's `delete_with_tag` returns a raw
167// `(Vec<u64>, bool)`; `Chisel::delete_with_tag` wraps it into this.
168// #[must_use] (ISSUES.md I121): `complete` is the field a resumable drop loops on.
169#[must_use = "TagDropProgress.complete tells you whether the tag drop finished; loop on it or bind with `let _`"]
170#[non_exhaustive]
171#[derive(Debug, Clone)]
172pub struct TagDropProgress {
173    /// Handles removed from the index in this pass. May be fewer than `max` if
174    /// the tag emptied first.
175    pub deleted: Vec<Handle>,
176    /// `true` if the tag has no remaining members after this pass.
177    pub complete: bool,
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn handle_round_trips_u64() {
186        let h = Handle::from(42);
187        assert_eq!(h.get(), 42);
188        assert_eq!(u64::from(h), 42);
189        assert_eq!(h, 42u64); // PartialEq<u64>
190        assert_eq!(42u64, h); // reverse direction
191    }
192
193    #[test]
194    fn handle_displays_as_raw() {
195        assert_eq!(format!("{}", Handle::from(7)), "7");
196    }
197
198    #[test]
199    fn tag_new_rejects_zero() {
200        assert_eq!(Tag::new(0), None);
201        assert_eq!(Tag::new(5).unwrap().get(), 5);
202    }
203
204    #[test]
205    fn tag_try_from_zero_errors() {
206        assert_eq!(Tag::try_from(0), Err(ZeroTagError));
207        assert_eq!(Tag::try_from(9).unwrap().get(), 9);
208    }
209
210    #[test]
211    fn tag_eq_and_display() {
212        let t = Tag::new(3).unwrap();
213        assert_eq!(t, 3u32);
214        assert_eq!(3u32, t);
215        assert_eq!(format!("{t}"), "3");
216        assert_eq!(u32::from(t), 3);
217    }
218}