Skip to main content

rustpython_vm/
intern.rs

1use rustpython_common::wtf8::{Wtf8, Wtf8Buf};
2
3use crate::{
4    AsObject, Py, PyExact, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, VirtualMachine,
5    builtins::{PyStr, PyStrInterned, PyTypeRef},
6    common::lock::PyRwLock,
7    convert::ToPyObject,
8};
9use alloc::borrow::ToOwned;
10use core::{borrow::Borrow, ops::Deref};
11
12#[derive(Debug)]
13pub(crate) struct StringPool {
14    inner: PyRwLock<std::collections::HashSet<CachedPyStrRef, rapidhash::quality::RandomState>>,
15}
16
17impl Default for StringPool {
18    fn default() -> Self {
19        Self {
20            inner: PyRwLock::new(Default::default()),
21        }
22    }
23}
24
25impl Clone for StringPool {
26    fn clone(&self) -> Self {
27        Self {
28            inner: PyRwLock::new(self.inner.read().clone()),
29        }
30    }
31}
32
33impl StringPool {
34    /// Reset the inner RwLock to unlocked state after fork().
35    ///
36    /// # Safety
37    /// Must only be called after fork() in the child process when no other
38    /// threads exist.
39    #[cfg(all(unix, feature = "threading", feature = "host_env"))]
40    pub(crate) unsafe fn reinit_after_fork(&self) {
41        unsafe { crate::common::lock::reinit_rwlock_after_fork(&self.inner) };
42    }
43
44    #[inline]
45    pub(crate) unsafe fn intern<S: InternableString>(
46        &self,
47        s: S,
48        typ: PyTypeRef,
49    ) -> &'static PyStrInterned {
50        if let Some(found) = self.interned(s.as_ref()) {
51            return found;
52        }
53
54        #[cold]
55        fn miss(zelf: &StringPool, s: PyRefExact<PyStr>) -> &'static PyStrInterned {
56            let cache = CachedPyStrRef { inner: s };
57            let mut inner = zelf.inner.write();
58            if inner.insert(cache.clone()) {
59                drop(inner);
60                let interned = unsafe { cache.as_interned_str() };
61                // `mark_intern` also makes the object immortal: the pool
62                // never gives an entry up and its refcount could already never
63                // reach zero, so this frees no memory that would otherwise
64                // have been freed — it only takes the atomic
65                // read-modify-write off every incref and decref of an
66                // attribute name, a dict key or a docstring.
67                unsafe { interned.as_object().mark_intern() };
68                interned
69            } else {
70                unsafe {
71                    inner
72                        .get(cache.as_ref())
73                        .expect("inserted is false")
74                        .as_interned_str()
75                }
76            }
77        }
78        let str_ref = s.into_pyref_exact(typ);
79        miss(self, str_ref)
80    }
81
82    #[inline]
83    pub(crate) fn interned<S: MaybeInternedString + ?Sized>(
84        &self,
85        s: &S,
86    ) -> Option<&'static PyStrInterned> {
87        if let Some(interned) = s.as_interned() {
88            return Some(interned);
89        }
90        self.inner
91            .read()
92            .get(s.as_ref())
93            .map(|cached| unsafe { cached.as_interned_str() })
94    }
95}
96
97#[derive(Debug, Clone)]
98#[repr(transparent)]
99pub(crate) struct CachedPyStrRef {
100    inner: PyRefExact<PyStr>,
101}
102
103impl core::hash::Hash for CachedPyStrRef {
104    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
105        self.inner.as_wtf8().hash(state)
106    }
107}
108
109impl PartialEq for CachedPyStrRef {
110    fn eq(&self, other: &Self) -> bool {
111        self.inner.as_wtf8() == other.inner.as_wtf8()
112    }
113}
114
115impl Eq for CachedPyStrRef {}
116
117impl core::borrow::Borrow<Wtf8> for CachedPyStrRef {
118    #[inline]
119    fn borrow(&self) -> &Wtf8 {
120        self.as_wtf8()
121    }
122}
123
124impl AsRef<Wtf8> for CachedPyStrRef {
125    #[inline]
126    fn as_ref(&self) -> &Wtf8 {
127        self.as_wtf8()
128    }
129}
130
131impl CachedPyStrRef {
132    /// # Safety
133    /// the given cache must be alive while returned reference is alive
134    #[inline]
135    const unsafe fn as_interned_str(&self) -> &'static PyStrInterned {
136        unsafe { core::mem::transmute_copy(self) }
137    }
138
139    #[inline]
140    fn as_wtf8(&self) -> &Wtf8 {
141        self.inner.as_wtf8()
142    }
143}
144
145#[repr(transparent)]
146pub struct PyInterned<T> {
147    inner: Py<T>,
148}
149
150impl PyInterned<PyStr> {
151    /// Returns `&str` for interned strings.
152    ///
153    /// # Panics
154    /// Panics if the interned string contains unpaired surrogates (WTF-8 content).
155    /// Most interned strings are valid UTF-8, so this is an ergonomic default.
156    #[inline]
157    pub fn as_str(&self) -> &str {
158        self.inner
159            .to_str()
160            .expect("interned str is always valid UTF-8")
161    }
162}
163
164impl<T: PyPayload> PyInterned<T> {
165    #[inline]
166    pub fn leak(cache: PyRef<T>) -> &'static Self {
167        unsafe { core::mem::transmute(cache) }
168    }
169
170    #[inline]
171    const fn as_ptr(&self) -> *const Py<T> {
172        self as *const _ as *const _
173    }
174
175    #[inline]
176    pub fn to_owned(&'static self) -> PyRef<T> {
177        unsafe { (*(&self as *const _ as *const PyRef<T>)).clone() }
178    }
179
180    #[inline]
181    pub fn to_object(&'static self) -> PyObjectRef {
182        self.to_owned().into()
183    }
184}
185
186impl<T: PyPayload> Borrow<PyObject> for PyInterned<T> {
187    #[inline(always)]
188    fn borrow(&self) -> &PyObject {
189        self.inner.borrow()
190    }
191}
192
193// NOTE: std::hash::Hash of Self and Self::Borrowed *must* be the same
194// This is ok only because PyObject doesn't implement Hash
195impl<T: PyPayload> core::hash::Hash for PyInterned<T> {
196    #[inline(always)]
197    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
198        self.get_id().hash(state)
199    }
200}
201
202impl<T> AsRef<Py<T>> for PyInterned<T> {
203    #[inline(always)]
204    fn as_ref(&self) -> &Py<T> {
205        &self.inner
206    }
207}
208
209impl<T> Deref for PyInterned<T> {
210    type Target = Py<T>;
211    #[inline(always)]
212    fn deref(&self) -> &Self::Target {
213        &self.inner
214    }
215}
216
217impl<T: PyPayload> PartialEq for PyInterned<T> {
218    #[inline(always)]
219    fn eq(&self, other: &Self) -> bool {
220        core::ptr::eq(self, other)
221    }
222}
223
224impl<T: PyPayload> Eq for PyInterned<T> {}
225
226impl<T: core::fmt::Debug + PyPayload> core::fmt::Debug for PyInterned<T> {
227    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
228        core::fmt::Debug::fmt(&**self, f)?;
229        write!(f, "@{:p}", self.as_ptr())
230    }
231}
232
233impl<T: PyPayload> ToPyObject for &'static PyInterned<T> {
234    fn to_pyobject(self, _vm: &VirtualMachine) -> PyObjectRef {
235        self.to_owned().into()
236    }
237}
238
239mod sealed {
240    use rustpython_common::wtf8::{Wtf8, Wtf8Buf};
241
242    use crate::{
243        builtins::PyStr,
244        object::{Py, PyExact, PyRefExact},
245    };
246
247    pub trait SealedInternable {}
248
249    impl SealedInternable for String {}
250    impl SealedInternable for &str {}
251    impl SealedInternable for Wtf8Buf {}
252    impl SealedInternable for &Wtf8 {}
253    impl SealedInternable for PyRefExact<PyStr> {}
254
255    pub trait SealedMaybeInterned {}
256
257    impl SealedMaybeInterned for str {}
258    impl SealedMaybeInterned for Wtf8 {}
259    impl SealedMaybeInterned for PyExact<PyStr> {}
260    impl SealedMaybeInterned for Py<PyStr> {}
261}
262
263/// A sealed marker trait for `DictKey` types that always become an exact instance of `str`
264pub trait InternableString: sealed::SealedInternable + ToPyObject + AsRef<Self::Interned> {
265    type Interned: MaybeInternedString + ?Sized;
266
267    fn into_pyref_exact(self, str_type: PyTypeRef) -> PyRefExact<PyStr>;
268}
269
270impl InternableString for String {
271    type Interned = str;
272    #[inline]
273    fn into_pyref_exact(self, str_type: PyTypeRef) -> PyRefExact<PyStr> {
274        let obj = PyRef::new_ref(PyStr::from(self), str_type, None);
275        unsafe { PyRefExact::new_unchecked(obj) }
276    }
277}
278
279impl InternableString for &str {
280    type Interned = str;
281
282    #[inline]
283    fn into_pyref_exact(self, str_type: PyTypeRef) -> PyRefExact<PyStr> {
284        self.to_owned().into_pyref_exact(str_type)
285    }
286}
287
288impl InternableString for Wtf8Buf {
289    type Interned = Wtf8;
290
291    fn into_pyref_exact(self, str_type: PyTypeRef) -> PyRefExact<PyStr> {
292        let obj = PyRef::new_ref(PyStr::from(self), str_type, None);
293        unsafe { PyRefExact::new_unchecked(obj) }
294    }
295}
296
297impl InternableString for &Wtf8 {
298    type Interned = Wtf8;
299
300    fn into_pyref_exact(self, str_type: PyTypeRef) -> PyRefExact<PyStr> {
301        self.to_owned().into_pyref_exact(str_type)
302    }
303}
304
305impl InternableString for PyRefExact<PyStr> {
306    type Interned = Py<PyStr>;
307
308    #[inline]
309    fn into_pyref_exact(self, _str_type: PyTypeRef) -> PyRefExact<PyStr> {
310        self
311    }
312}
313
314pub trait MaybeInternedString:
315    AsRef<Wtf8> + crate::dict_inner::DictKey + sealed::SealedMaybeInterned
316{
317    fn as_interned(&self) -> Option<&'static PyStrInterned>;
318}
319
320impl MaybeInternedString for str {
321    #[inline(always)]
322    fn as_interned(&self) -> Option<&'static PyStrInterned> {
323        None
324    }
325}
326
327impl MaybeInternedString for Wtf8 {
328    #[inline(always)]
329    fn as_interned(&self) -> Option<&'static PyStrInterned> {
330        None
331    }
332}
333
334impl MaybeInternedString for PyExact<PyStr> {
335    #[inline(always)]
336    fn as_interned(&self) -> Option<&'static PyStrInterned> {
337        None
338    }
339}
340
341impl MaybeInternedString for Py<PyStr> {
342    #[inline(always)]
343    fn as_interned(&self) -> Option<&'static PyStrInterned> {
344        if self.as_object().is_interned() {
345            Some(unsafe { core::mem::transmute::<&Self, &PyInterned<PyStr>>(self) })
346        } else {
347            None
348        }
349    }
350}
351
352impl PyObject {
353    #[inline]
354    pub fn as_interned_str(&self, vm: &crate::VirtualMachine) -> Option<&'static PyStrInterned> {
355        let s: Option<&Py<PyStr>> = self.downcast_ref();
356        if self.is_interned() {
357            s.unwrap().as_interned()
358        } else if let Some(s) = s {
359            vm.ctx.interned_str(s.as_wtf8())
360        } else {
361            None
362        }
363    }
364}