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}