Skip to content

Commit d1bcab9

Browse files
authored
Merge pull request #2964 from ruby/rust-constant-pool-typename
[rust] Add typename
2 parents 1dfe419 + b0d2358 commit d1bcab9

6 files changed

Lines changed: 1142 additions & 4 deletions

File tree

rust/Cargo.lock

Lines changed: 11 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/ruby-rbs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ include = [
1616

1717
[dependencies]
1818
ruby-rbs-sys = { version = "0.3", path = "../ruby-rbs-sys" }
19+
xxhash-rust = { version = "0.8", features = ["xxh3"] }
1920

2021
[build-dependencies]
2122
serde = { version = "1.0", features = ["derive"] }

rust/ruby-rbs/src/ids.rs

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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+
/// Tag for IDs produced by the string interner.
122+
pub enum SymbolTag {}
123+
124+
/// Identifier for an interned string. Content-addressed: `xxh3_64` of the
125+
/// string bytes.
126+
pub type SymbolId = Id<SymbolTag>;
127+
128+
/// Tag for IDs produced by the type-name interner.
129+
pub enum TypeNameTag {}
130+
131+
/// Identifier for an interned type name. Content-addressed: derived from
132+
/// the parent type name's hash and the last segment's hash.
133+
pub type TypeName = Id<TypeNameTag>;
134+
135+
#[cfg(test)]
136+
mod tests {
137+
use super::*;
138+
139+
enum FooTag {}
140+
enum BarTag {}
141+
type FooId = Id<FooTag>;
142+
type BarId = Id<BarTag>;
143+
144+
#[test]
145+
fn from_hash_preserves_nonzero_value() {
146+
let id = FooId::from_hash(42);
147+
assert_eq!(id.get(), 42);
148+
}
149+
150+
#[test]
151+
fn from_hash_folds_zero_to_one() {
152+
let id = FooId::from_hash(0);
153+
assert_eq!(id.get(), 1);
154+
}
155+
156+
#[test]
157+
fn option_is_niche_optimized() {
158+
assert_eq!(
159+
std::mem::size_of::<Option<FooId>>(),
160+
std::mem::size_of::<FooId>(),
161+
);
162+
assert_eq!(std::mem::size_of::<FooId>(), 8);
163+
}
164+
165+
#[test]
166+
fn equality_and_ordering() {
167+
let a = FooId::from_hash(10);
168+
let b = FooId::from_hash(10);
169+
let c = FooId::from_hash(20);
170+
assert_eq!(a, b);
171+
assert_ne!(a, c);
172+
assert!(a < c);
173+
}
174+
175+
#[test]
176+
fn different_tags_are_distinct_types() {
177+
let _foo = FooId::from_hash(1);
178+
let _bar = BarId::from_hash(1);
179+
// The following would not compile:
180+
// let _: FooId = _bar;
181+
}
182+
183+
#[test]
184+
fn debug_shows_tag_name_and_hex() {
185+
let foo = FooId::from_hash(0xDEAD_BEEF);
186+
assert_eq!(format!("{foo:?}"), "FooTag(0x00000000deadbeef)");
187+
}
188+
}

0 commit comments

Comments
 (0)