Skip to content

Commit 7807b0c

Browse files
committed
Add ids module
1 parent 2479202 commit 7807b0c

2 files changed

Lines changed: 175 additions & 0 deletions

File tree

rust/ruby-rbs/src/ids.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
//! Content-addressed, type-tagged 64-bit IDs.
2+
//!
3+
//! Different ID domains (interned strings, type names, ...) share the same
4+
//! underlying `NonZeroU64` representation but are distinguished at the
5+
//! type level via a phantom tag, so a `SymbolId` cannot be silently used
6+
//! where a `TypeName` is expected.
7+
//!
8+
//! The value of an ID is the 64-bit xxh3 hash of its content (with `0`
9+
//! folded to `1` to keep the niche optimization on `Option<Id<T>>`).
10+
//! Because IDs are derived from content, two independently-built
11+
//! interners that see the same value assign the same ID — merging is
12+
//! just a `HashMap` union, no remap walk needed.
13+
//!
14+
//! ```
15+
//! use ruby_rbs::ids::{Id, SymbolId};
16+
//! enum OtherTag {}
17+
//! type OtherId = Id<OtherTag>;
18+
//!
19+
//! fn takes_symbol(_: SymbolId) {}
20+
//! // takes_symbol(OtherId::from_hash(0)); // compile error
21+
//! ```
22+
23+
use std::cmp::Ordering;
24+
use std::hash::{Hash, Hasher};
25+
use std::marker::PhantomData;
26+
use std::num::NonZeroU64;
27+
28+
/// A 64-bit content-addressed ID tagged with a domain marker `T`.
29+
///
30+
/// The tag is a zero-sized type parameter — typically an uninhabited enum —
31+
/// used only to distinguish ID domains at the type level.
32+
pub struct Id<T> {
33+
raw: NonZeroU64,
34+
_tag: PhantomData<fn() -> T>,
35+
}
36+
37+
impl<T> Id<T> {
38+
/// Wrap a 64-bit hash as an `Id`. A hash of `0` is folded to `1`
39+
/// so the representation stays non-zero (enabling niche optimization
40+
/// in `Option<Id<T>>`). Collisions on the folded value are vanishingly
41+
/// rare in 2^64 space.
42+
#[must_use]
43+
pub fn from_hash(h: u64) -> Self {
44+
let raw = NonZeroU64::new(h).unwrap_or(NonZeroU64::new(1).unwrap());
45+
Self {
46+
raw,
47+
_tag: PhantomData,
48+
}
49+
}
50+
51+
/// Wrap a pre-validated non-zero value.
52+
#[must_use]
53+
pub fn from_raw(raw: NonZeroU64) -> Self {
54+
Self {
55+
raw,
56+
_tag: PhantomData,
57+
}
58+
}
59+
60+
/// The underlying non-zero 64-bit value.
61+
#[must_use]
62+
pub fn raw(self) -> NonZeroU64 {
63+
self.raw
64+
}
65+
66+
/// The underlying 64-bit value as a plain integer.
67+
#[must_use]
68+
pub fn get(self) -> u64 {
69+
self.raw.get()
70+
}
71+
}
72+
73+
// Manual trait impls — `#[derive(...)]` on a struct with `PhantomData<T>`
74+
// would add unnecessary bounds on `T`, but `Id<T>` is always copyable,
75+
// hashable, etc. regardless of the tag.
76+
77+
impl<T> Copy for Id<T> {}
78+
79+
impl<T> Clone for Id<T> {
80+
fn clone(&self) -> Self {
81+
*self
82+
}
83+
}
84+
85+
impl<T> PartialEq for Id<T> {
86+
fn eq(&self, other: &Self) -> bool {
87+
self.raw == other.raw
88+
}
89+
}
90+
91+
impl<T> Eq for Id<T> {}
92+
93+
impl<T> PartialOrd for Id<T> {
94+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
95+
Some(self.cmp(other))
96+
}
97+
}
98+
99+
impl<T> Ord for Id<T> {
100+
fn cmp(&self, other: &Self) -> Ordering {
101+
self.raw.cmp(&other.raw)
102+
}
103+
}
104+
105+
impl<T> Hash for Id<T> {
106+
fn hash<H: Hasher>(&self, state: &mut H) {
107+
self.raw.hash(state);
108+
}
109+
}
110+
111+
impl<T> std::fmt::Debug for Id<T> {
112+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113+
let tag = std::any::type_name::<T>()
114+
.rsplit("::")
115+
.next()
116+
.unwrap_or("Id");
117+
write!(f, "{}({:#018x})", tag, self.raw.get())
118+
}
119+
}
120+
121+
#[cfg(test)]
122+
mod tests {
123+
use super::*;
124+
125+
enum FooTag {}
126+
enum BarTag {}
127+
type FooId = Id<FooTag>;
128+
type BarId = Id<BarTag>;
129+
130+
#[test]
131+
fn from_hash_preserves_nonzero_value() {
132+
let id = FooId::from_hash(42);
133+
assert_eq!(id.get(), 42);
134+
}
135+
136+
#[test]
137+
fn from_hash_folds_zero_to_one() {
138+
let id = FooId::from_hash(0);
139+
assert_eq!(id.get(), 1);
140+
}
141+
142+
#[test]
143+
fn option_is_niche_optimized() {
144+
assert_eq!(
145+
std::mem::size_of::<Option<FooId>>(),
146+
std::mem::size_of::<FooId>(),
147+
);
148+
assert_eq!(std::mem::size_of::<FooId>(), 8);
149+
}
150+
151+
#[test]
152+
fn equality_and_ordering() {
153+
let a = FooId::from_hash(10);
154+
let b = FooId::from_hash(10);
155+
let c = FooId::from_hash(20);
156+
assert_eq!(a, b);
157+
assert_ne!(a, c);
158+
assert!(a < c);
159+
}
160+
161+
#[test]
162+
fn different_tags_are_distinct_types() {
163+
let _foo = FooId::from_hash(1);
164+
let _bar = BarId::from_hash(1);
165+
// The following would not compile:
166+
// let _: FooId = _bar;
167+
}
168+
169+
#[test]
170+
fn debug_shows_tag_name_and_hex() {
171+
let foo = FooId::from_hash(0xDEAD_BEEF);
172+
assert_eq!(format!("{foo:?}"), "FooTag(0x00000000deadbeef)");
173+
}
174+
}

rust/ruby-rbs/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
pub mod ids;
12
pub mod node;

0 commit comments

Comments
 (0)