Skip to main content

catallaxy_runtime/
object.rs

1//! Heap object layouts and their GC hooks.
2//!
3//! Every object starts with a [`Header`]. Instances keep their fields inline as atomic words, so a
4//! racing reader can never see a torn value. Lists do the same with their elements and swap whole
5//! buffers on growth, so their reads never lock. Dicts and other containers keep Rust-side
6//! buffers; the ownership rules (and per-object locks for shared objects) guarantee those are
7//! never mutated while another thread reads them.
8
9use std::cell::UnsafeCell;
10use std::sync::atomic::{AtomicPtr, AtomicU32, AtomicU64, AtomicUsize, Ordering};
11
12use catallaxy_gc::{GcVTable, Header, Tracer, HEADER_SIZE};
13use num_bigint::BigInt;
14
15use crate::buf::WordBuf;
16use crate::class::{heap_class, ObjKind};
17use crate::dict::DictTable;
18use crate::ownership::Access;
19use crate::value::Value;
20
21#[inline(always)]
22pub fn mark_value(t: &mut Tracer, v: Value) {
23    if v.is_ptr() {
24        t.mark(v.as_ptr());
25    }
26}
27
28#[repr(C)]
29pub struct InstanceObj {
30    pub header: Header,
31    fields: [AtomicU64; 0],
32}
33
34impl InstanceObj {
35    #[inline(always)]
36    pub fn field(&self, i: usize) -> &AtomicU64 {
37        // SAFETY: instances are allocated with `nfields` trailing words and callers index within it.
38        unsafe { &*self.fields.as_ptr().add(i) }
39    }
40    #[inline(always)]
41    pub fn get(&self, i: usize) -> Value {
42        Value(self.field(i).load(Ordering::Relaxed))
43    }
44    #[inline(always)]
45    pub fn get_acquire(&self, i: usize) -> Value {
46        Value(self.field(i).load(Ordering::Acquire))
47    }
48    #[inline(always)]
49    pub fn set(&self, i: usize, v: Value) {
50        self.field(i).store(v.0, Ordering::Relaxed)
51    }
52    #[inline(always)]
53    pub fn set_release(&self, i: usize, v: Value) {
54        self.field(i).store(v.0, Ordering::Release)
55    }
56    pub fn nfields(&self) -> usize {
57        unsafe { heap_class(&self.header).nfields() }
58    }
59    pub fn size_for(nfields: usize) -> usize {
60        HEADER_SIZE + 8 * nfields
61    }
62}
63
64/// A string: immutable UTF-8 bytes with a cached hash and character count.
65///
66/// Strings of at least [`StrObj::INDEX_MIN`] bytes get their character count when they are
67/// allocated. When such a string is not ASCII, its allocation also reserves a sparse index after
68/// the bytes: one `u32` byte offset for every [`StrObj::STRIDE`]th character, filled in the first
69/// time a character position is looked up. That makes `s[i]` and slicing O(1) for ASCII strings
70/// (character count equals byte length) and O(`STRIDE`) for the rest.
71#[repr(C)]
72pub struct StrObj {
73    pub header: Header,
74    pub len: usize,
75    /// Cached hash; 0 means not yet computed.
76    pub hash: AtomicU64,
77    /// Cached char count plus one (0 means not yet computed), and [`StrObj::INDEX_READY`] once
78    /// the char index has been filled in.
79    pub chars: AtomicU64,
80    data: [u8; 0],
81}
82
83impl StrObj {
84    pub const BASE: usize = std::mem::size_of::<StrObj>();
85    /// Strings at least this long have an eagerly computed char count, and an index if not ASCII.
86    pub const INDEX_MIN: usize = 256;
87    /// Characters per index entry.
88    pub const STRIDE: usize = 64;
89    const INDEX_READY: u64 = 1 << 63;
90
91    /// The allocation size of a string of `len` bytes and `chars` characters, and the initial
92    /// value of its `chars` word. `chars` must be known (`Some`) when `len >= INDEX_MIN`.
93    pub(crate) fn layout(len: usize, chars: Option<usize>) -> (usize, u64) {
94        let word = chars.map_or(0, |c| c as u64 + 1);
95        (Self::size_with(len, word), word)
96    }
97
98    /// Number of index entries for a string of `len` bytes whose `chars` word is `word`.
99    fn index_entries(len: usize, word: u64) -> usize {
100        let word = word & !Self::INDEX_READY;
101        if len < Self::INDEX_MIN || len > u32::MAX as usize || word == 0 || (word - 1) as usize == len {
102            return 0;
103        }
104        ((word - 1) as usize).div_ceil(Self::STRIDE)
105    }
106
107    fn size_with(len: usize, word: u64) -> usize {
108        match Self::index_entries(len, word) {
109            0 => Self::BASE + len,
110            n => Self::index_offset(len) + 4 * n,
111        }
112    }
113
114    /// Total bytes the object occupies (header, bytes and index padding included).
115    pub fn alloc_size(&self) -> usize {
116        Self::size_with(self.len, self.chars.load(Ordering::Relaxed))
117    }
118
119    pub fn as_str(&self) -> &str {
120        // SAFETY: strings are created from valid UTF-8 and never mutated.
121        unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.data.as_ptr(), self.len)) }
122    }
123    pub fn char_count(&self) -> usize {
124        let c = self.chars.load(Ordering::Relaxed) & !Self::INDEX_READY;
125        if c != 0 {
126            return (c - 1) as usize;
127        }
128        let s = self.as_str();
129        let n = if s.is_ascii() { s.len() } else { s.chars().count() };
130        // Only strings shorter than INDEX_MIN get here, and they have no index bit to lose.
131        self.chars.store(n as u64 + 1, Ordering::Relaxed);
132        n
133    }
134    /// Whether every character is a single byte. O(1) once the char count is known.
135    #[inline]
136    pub fn is_ascii(&self) -> bool {
137        self.char_count() == self.len
138    }
139
140    /// The index slots, if this string has an index.
141    fn index(&self) -> Option<&[AtomicU32]> {
142        let n = Self::index_entries(self.len, self.chars.load(Ordering::Relaxed));
143        if n == 0 {
144            return None;
145        }
146        // SAFETY: the allocation reserved `n` slots right after the (4-aligned) bytes; they
147        // start zeroed and are only ever accessed atomically.
148        unsafe {
149            let base = (self as *const StrObj as *const u8).add(Self::index_offset(self.len));
150            Some(std::slice::from_raw_parts(base as *const AtomicU32, n))
151        }
152    }
153
154    fn index_offset(len: usize) -> usize {
155        (Self::BASE + len).next_multiple_of(4)
156    }
157
158    /// The filled-in index. Racing builders store identical values.
159    fn ready_index(&self) -> Option<&[AtomicU32]> {
160        let ix = self.index()?;
161        if self.chars.load(Ordering::Acquire) & Self::INDEX_READY == 0 {
162            for (slot, (b, _)) in ix.iter().zip(self.as_str().char_indices().step_by(Self::STRIDE)) {
163                slot.store(b as u32, Ordering::Relaxed);
164            }
165            self.chars.fetch_or(Self::INDEX_READY, Ordering::Release);
166        }
167        Some(ix)
168    }
169
170    /// The byte offset of character `i` (`i` may equal the char count, giving the length).
171    ///
172    /// O(1) for ASCII strings, O([`StrObj::STRIDE`]) for indexed ones, O(n) for short ones.
173    pub fn byte_offset(&self, i: usize) -> usize {
174        let n = self.char_count();
175        if n == self.len || i == 0 {
176            return i.min(self.len);
177        }
178        if i >= n {
179            return self.len;
180        }
181        let bytes = self.as_str().as_bytes();
182        let (mut b, mut skip) = match self.ready_index() {
183            Some(ix) => (ix[i / Self::STRIDE].load(Ordering::Relaxed) as usize, i % Self::STRIDE),
184            None => (0, i),
185        };
186        while skip > 0 {
187            b += 1;
188            while bytes[b] & 0xC0 == 0x80 {
189                b += 1;
190            }
191            skip -= 1;
192        }
193        b
194    }
195
196    /// The character position of byte offset `b`, which must be a char boundary.
197    pub fn char_offset(&self, b: usize) -> usize {
198        if self.is_ascii() {
199            return b;
200        }
201        let s = self.as_str();
202        let (c0, b0) = match self.ready_index() {
203            Some(ix) => {
204                // The last entry at or before `b`.
205                let k = ix.partition_point(|x| x.load(Ordering::Relaxed) as usize <= b).max(1) - 1;
206                (k * Self::STRIDE, ix[k].load(Ordering::Relaxed) as usize)
207            }
208            None => (0, 0),
209        };
210        c0 + s[b0..b].chars().count()
211    }
212
213    /// The `i`th character (`i < char_count()`).
214    pub fn char_at(&self, i: usize) -> &str {
215        let b = self.byte_offset(i);
216        let s = self.as_str();
217        let w = if self.is_ascii() { 1 } else { s[b..].chars().next().map_or(0, char::len_utf8) };
218        &s[b..b + w]
219    }
220
221    /// The characters `[start, end)` (clamped to the string).
222    pub fn char_slice(&self, start: usize, end: usize) -> &str {
223        let n = self.char_count();
224        let (start, end) = (start.min(n), end.min(n));
225        if end <= start {
226            return "";
227        }
228        let a = self.byte_offset(start);
229        &self.as_str()[a..self.byte_offset(end)]
230    }
231
232    pub fn hash_code(&self) -> u64 {
233        let h = self.hash.load(Ordering::Relaxed);
234        if h != 0 {
235            return h;
236        }
237        let mut h = crate::ops::hash_bytes(self.as_str().as_bytes());
238        if h == 0 {
239            h = 1;
240        }
241        self.hash.store(h, Ordering::Relaxed);
242        h
243    }
244}
245
246#[repr(C)]
247pub struct BigIntObj {
248    pub header: Header,
249    pub value: BigInt,
250}
251
252/// A list: header, length, a pointer to a backing array, and room for a few elements inline.
253///
254/// Reads never lock, even on shared lists. The length and buffer pointer are atomic words, and a
255/// reader bounds-checks against the buffer it loaded, so it sees some consistent-enough state and
256/// never reads out of bounds. A writer (the owner, or a thread holding a shared list's lock) that
257/// outgrows the buffer copies it into a new one and swaps the pointer; the old buffer is retired
258/// to the collector, which frees it only once the world is stopped and no reader can hold it.
259///
260/// Small lists keep their elements in the object itself: `inline` is a [`WordBuf`] embedded at
261/// the end of the object (its slots follow the struct), and `buf` points at it. Readers do not
262/// tell the two cases apart; they load `buf` as always. A list that outgrows its inline slots
263/// moves to an out-of-line buffer exactly as above, and never moves back, so the inline slots are
264/// never written again. A racing reader that loaded the inline pointer before the move reads
265/// values the list held then, from memory that lives as long as the list does, so the inline
266/// buffer needs no retiring. It is never freed on its own: it goes with the object.
267#[repr(C)]
268pub struct ListObj {
269    pub header: Header,
270    buf: AtomicPtr<WordBuf>,
271    /// Just before `inline`: where an out-of-line buffer keeps its written range, the inline
272    /// buffer has the length, which reads as the range `0..len` (see `note_written`).
273    len: AtomicUsize,
274    /// The inline buffer's capacity word; its slots follow the struct. Capacity 0 for lists
275    /// created out of line.
276    inline: WordBuf,
277}
278
279const _: () = assert!(std::mem::offset_of!(ListObj, inline) == std::mem::offset_of!(ListObj, len) + 8);
280
281// Size classes are 16-byte steps; the inline capacities chosen below (odd) fill the object's
282// slot exactly only if the struct ends half way through a step.
283const _: () = assert!(std::mem::size_of::<ListObj>() % 16 == 8 && ListObj::INLINE_MAX % 2 == 1);
284
285impl ListObj {
286    /// Most elements a new list keeps inline (a 96-byte object). Odd, so the object fills its
287    /// size class. Chosen by measurement (issue #104).
288    pub const INLINE_MAX: usize = 7;
289    /// Inline capacity of a new empty (or one-element) list.
290    pub const INLINE_MIN: usize = 3;
291
292    /// The inline capacity of a new list that should have room for `cap` elements, or 0 if
293    /// they belong out of line. Rounded up to odd: an even count would leave a word of the
294    /// size-class slot unused.
295    #[inline]
296    pub fn inline_cap_for(cap: usize) -> usize {
297        if cap > Self::INLINE_MAX {
298            0
299        } else {
300            cap.max(Self::INLINE_MIN) | 1
301        }
302    }
303
304    /// The allocation size of a list with inline capacity `icap`.
305    #[inline]
306    pub fn size_for(icap: usize) -> usize {
307        std::mem::size_of::<ListObj>() + 8 * icap
308    }
309
310    /// # Safety
311    /// `p` must point to freshly allocated, unpublished list memory of
312    /// [`ListObj::size_for`]`(icap)` bytes.
313    pub(crate) unsafe fn init(p: *mut ListObj, items: Vec<Value>, icap: usize) {
314        if let Err(layout) = Self::try_init(p, items, icap) {
315            std::alloc::handle_alloc_error(layout);
316        }
317    }
318
319    /// As [`ListObj::init`], but if the buffer cannot be had, frees `items`, leaves an empty
320    /// list and returns the layout the allocator refused.
321    ///
322    /// # Safety
323    /// As for [`ListObj::init`].
324    pub(crate) unsafe fn try_init(p: *mut ListObj, items: Vec<Value>, icap: usize) -> Result<(), std::alloc::Layout> {
325        let len = items.len();
326        if icap >= len && icap > 0 {
327            // Small: copy into the object and give the vector's allocation back.
328            Self::init_inline(p, items.iter().map(|v| v.0), icap, len);
329            return Ok(());
330        }
331        Self::init_inline(p, std::iter::empty(), 0, 0);
332        // `Value` is a transparent `u64`, so the vector's allocation is reused as is.
333        let mut items = std::mem::ManuallyDrop::new(items);
334        let words = Vec::from_raw_parts(items.as_mut_ptr() as *mut u64, len, items.capacity());
335        match WordBuf::try_from_vec(words, Value::NIL.0) {
336            Ok(buf) => Self::init_buf(p, buf, len),
337            Err(layout) => {
338                Self::init_buf(p, WordBuf::empty(), 0);
339                return Err(layout);
340            }
341        }
342        Ok(())
343    }
344
345    /// As [`ListObj::init`], copying `items` into a buffer of `cap` (at least `items.len()`):
346    /// the inline one if `icap` is at least `cap`.
347    ///
348    /// # Safety
349    /// As for [`ListObj::init`].
350    pub(crate) unsafe fn init_from_slice(p: *mut ListObj, items: &[Value], cap: usize, icap: usize) {
351        if let Err(layout) = Self::try_init_from_slice(p, items, cap, icap) {
352            std::alloc::handle_alloc_error(layout);
353        }
354    }
355
356    /// As [`ListObj::init_from_slice`], but if the buffer cannot be had, leaves an empty list
357    /// and returns the layout the allocator refused.
358    ///
359    /// # Safety
360    /// As for [`ListObj::init`].
361    pub(crate) unsafe fn try_init_from_slice(
362        p: *mut ListObj,
363        items: &[Value],
364        cap: usize,
365        icap: usize,
366    ) -> Result<(), std::alloc::Layout> {
367        let cap = cap.max(items.len());
368        if icap >= cap && icap > 0 {
369            Self::init_inline(p, items.iter().map(|v| v.0), icap, items.len());
370            return Ok(());
371        }
372        Self::init_inline(p, std::iter::empty(), 0, 0);
373        match WordBuf::try_alloc_from(cap, items.iter().map(|v| v.0), Value::NIL.0) {
374            Ok(buf) => Self::init_buf(p, buf, items.len()),
375            Err(layout) => {
376                Self::init_buf(p, WordBuf::empty(), 0);
377                return Err(layout);
378            }
379        }
380        Ok(())
381    }
382
383    /// Writes the inline buffer (`icap` slots holding `items`, then nil) and, if `icap > 0`,
384    /// points the list at it.
385    unsafe fn init_inline(p: *mut ListObj, items: impl IntoIterator<Item = u64>, icap: usize, len: usize) {
386        let inline = std::ptr::addr_of_mut!((*p).inline);
387        WordBuf::init_in_place(inline, icap, items, Value::NIL.0);
388        if icap > 0 {
389            Self::init_buf(p, inline, len);
390        }
391    }
392
393    unsafe fn init_buf(p: *mut ListObj, buf: *mut WordBuf, len: usize) {
394        std::ptr::addr_of_mut!((*p).len).write(AtomicUsize::new(len));
395        std::ptr::addr_of_mut!((*p).buf).write(AtomicPtr::new(buf));
396    }
397
398    /// The address of the inline buffer, for comparison only.
399    #[inline(always)]
400    fn inline_addr(&self) -> *mut WordBuf {
401        std::ptr::addr_of!(self.inline) as *mut WordBuf
402    }
403
404    /// Whether the elements are currently stored in the object itself.
405    pub fn is_inline(&self) -> bool {
406        self.buf.load(Ordering::Acquire) == self.inline_addr()
407    }
408
409    #[inline(always)]
410    pub fn len(&self) -> usize {
411        self.len.load(Ordering::Acquire)
412    }
413
414    #[inline(always)]
415    pub fn is_empty(&self) -> bool {
416        self.len() == 0
417    }
418
419    #[inline(always)]
420    fn slots(&self) -> &[AtomicU64] {
421        unsafe { WordBuf::slots(self.buf.load(Ordering::Acquire)) }
422    }
423
424    /// The item at `i`, if in bounds. Safe against concurrent writers; the caller must still have
425    /// passed the ownership read check.
426    #[inline(always)]
427    pub fn get(&self, i: usize) -> Option<Value> {
428        if i >= self.len() {
429            return None;
430        }
431        self.slots().get(i).map(|x| Value(x.load(Ordering::Acquire)))
432    }
433
434    /// As [`ListObj::get`] for a list owned by this thread or frozen, where nothing races.
435    #[inline(always)]
436    pub fn get_local(&self, i: usize) -> Option<Value> {
437        if i >= self.len.load(Ordering::Relaxed) {
438            return None;
439        }
440        let slots = unsafe { WordBuf::slots(self.buf.load(Ordering::Relaxed)) };
441        slots.get(i).map(|x| Value(x.load(Ordering::Relaxed)))
442    }
443
444    /// A snapshot of the items.
445    pub fn to_vec(&self) -> Vec<Value> {
446        let n = self.len();
447        let slots = self.slots();
448        slots[..n.min(slots.len())].iter().map(|x| Value(x.load(Ordering::Acquire))).collect()
449    }
450
451    /// As [`ListObj::to_vec`], raising a `MemoryError` if the vector cannot be had.
452    pub fn try_to_vec(&self) -> crate::RtResult<Vec<Value>> {
453        let n = self.len();
454        let slots = self.slots();
455        let items = &slots[..n.min(slots.len())];
456        let mut v = crate::try_vec(items.len())?;
457        v.extend(items.iter().map(|x| Value(x.load(Ordering::Acquire))));
458        Ok(v)
459    }
460
461    pub fn capacity(&self) -> usize {
462        unsafe { WordBuf::cap(self.buf.load(Ordering::Acquire)) }
463    }
464
465    // Writers. Each requires write access per the ownership rules: the owning thread, or the
466    // holder of a shared list's lock. `access` says which, so replaced buffers are freed at once
467    // when no other thread can be reading them.
468
469    /// # Safety
470    /// See above.
471    pub unsafe fn set(&self, i: usize, v: Value) -> bool {
472        if i >= self.len.load(Ordering::Relaxed) {
473            return false;
474        }
475        let buf = self.buf.load(Ordering::Relaxed);
476        WordBuf::slots(buf)[i].store(v.0, Ordering::Release);
477        self.note_store(buf, i, v);
478        true
479    }
480
481    /// The slots a minor collection would scan if the list were remembered: all of them when
482    /// inline, else those recorded as written since the last one. For tests.
483    #[doc(hidden)]
484    pub fn written_range(&self) -> std::ops::Range<usize> {
485        let buf = self.buf.load(Ordering::Acquire);
486        if buf == self.inline_addr() {
487            return 0..self.len();
488        }
489        // SAFETY: an out-of-line buffer is allocated by `WordBuf`.
490        unsafe { WordBuf::written(buf) }
491    }
492
493    /// Records slots `lo..hi` of `buf`, the current buffer, as written (see
494    /// [`WordBuf::note_written`]).
495    ///
496    /// A young list records nothing: its buffer's range covers every slot (a new buffer's does,
497    /// and only a minor collection of an old list narrows it), so the common case costs one test
498    /// of a header bit. Nor does the inline buffer: it is small enough that a minor collection
499    /// scans it whole. It has no range word, but the word before it is the length, which reads as
500    /// the range `0..len`; so storing into an existing element passes the check below without
501    /// telling the two cases apart, and only the slow path does.
502    #[inline(always)]
503    unsafe fn note_written(&self, buf: *mut WordBuf, lo: usize, hi: usize) {
504        if !self.header.is_young() && !WordBuf::range_covers(WordBuf::range_word(buf), lo, hi) {
505            self.widen_written(buf, lo, hi);
506        }
507    }
508
509    /// Records slot `i` of `buf`, the current buffer, as written with `v`. The range only has to
510    /// cover slots that may hold a young reference, so storing a number or an old object records
511    /// nothing (a slot that held a young reference before is still in the range).
512    #[inline(always)]
513    unsafe fn note_store(&self, buf: *mut WordBuf, i: usize, v: Value) {
514        if v.header().is_some_and(|h| h.is_young())
515            && !self.header.is_young()
516            && !WordBuf::range_covers(WordBuf::range_word(buf), i, i + 1)
517        {
518            self.widen_written(buf, i, i + 1);
519        }
520    }
521
522    #[cold]
523    #[inline(never)]
524    unsafe fn widen_written(&self, buf: *mut WordBuf, lo: usize, hi: usize) {
525        if buf != self.inline_addr() {
526            WordBuf::widen_written(buf, lo, hi);
527        }
528    }
529
530    /// # Safety
531    /// See above.
532    pub unsafe fn push(&self, v: Value, access: Access) {
533        if let Err(layout) = self.try_push(v, access) {
534            std::alloc::handle_alloc_error(layout);
535        }
536    }
537
538    /// As [`ListObj::push`], but if the allocator refuses to grow the list, returns the
539    /// refused layout and leaves the list as it was.
540    ///
541    /// # Safety
542    /// See above.
543    pub unsafe fn try_push(&self, v: Value, access: Access) -> Result<(), std::alloc::Layout> {
544        let n = self.len.load(Ordering::Relaxed);
545        let mut buf = self.buf.load(Ordering::Relaxed);
546        if n == WordBuf::cap(buf) {
547            self.grow(n + 1, access)?;
548            buf = self.buf.load(Ordering::Relaxed);
549        }
550        WordBuf::slots(buf)[n].store(v.0, Ordering::Release);
551        self.note_store(buf, n, v);
552        self.len.store(n + 1, Ordering::Release);
553        Ok(())
554    }
555
556    /// # Safety
557    /// See above.
558    pub unsafe fn pop(&self) -> Option<Value> {
559        let n = self.len.load(Ordering::Relaxed);
560        if n == 0 {
561            return None;
562        }
563        let slot = &self.slots()[n - 1];
564        let v = Value(slot.load(Ordering::Relaxed));
565        self.len.store(n - 1, Ordering::Release);
566        slot.store(Value::NIL.0, Ordering::Release);
567        Some(v)
568    }
569
570    /// Replaces the contents with `items`, reusing the buffer when it is large enough.
571    ///
572    /// # Safety
573    /// See above.
574    pub unsafe fn replace(&self, items: &[Value], access: Access) {
575        if let Err(layout) = self.try_replace(items, access) {
576            std::alloc::handle_alloc_error(layout);
577        }
578    }
579
580    /// As [`ListObj::replace`], but if the allocator refuses a larger buffer, returns the
581    /// refused layout and leaves the list as it was.
582    ///
583    /// # Safety
584    /// See above.
585    pub unsafe fn try_replace(&self, items: &[Value], access: Access) -> Result<(), std::alloc::Layout> {
586        let old_len = self.len.load(Ordering::Relaxed);
587        let n = items.len();
588        if n > self.capacity() {
589            // A new buffer counts as written throughout.
590            let buf = WordBuf::try_alloc_from(n.max(4), items.iter().map(|v| v.0), Value::NIL.0)?;
591            self.swap_buf(buf, access);
592            self.len.store(n, Ordering::Release);
593            return Ok(());
594        }
595        let buf = self.buf.load(Ordering::Relaxed);
596        if n > 0 {
597            self.note_written(buf, 0, n);
598        }
599        let slots = WordBuf::slots(buf);
600        if n < old_len {
601            self.len.store(n, Ordering::Release);
602        }
603        for (s, v) in slots.iter().zip(items) {
604            s.store(v.0, Ordering::Release);
605        }
606        for s in &slots[n..old_len.max(n)] {
607            s.store(Value::NIL.0, Ordering::Release);
608        }
609        if n >= old_len {
610            self.len.store(n, Ordering::Release);
611        }
612        Ok(())
613    }
614
615    /// Runs `f` on a copy of the items and writes the result back.
616    ///
617    /// # Safety
618    /// See above.
619    pub unsafe fn modify<R>(&self, access: Access, f: impl FnOnce(&mut Vec<Value>) -> R) -> R {
620        let mut items = self.to_vec();
621        let r = f(&mut items);
622        self.replace(&items, access);
623        r
624    }
625
626    /// As [`ListObj::modify`], but if the allocator refuses a larger buffer for the result,
627    /// returns the refused layout and leaves the list as it was.
628    ///
629    /// # Safety
630    /// See above.
631    pub unsafe fn try_modify<R>(
632        &self,
633        access: Access,
634        f: impl FnOnce(&mut Vec<Value>) -> R,
635    ) -> Result<R, std::alloc::Layout> {
636        // The copy, with room for one more item (`insert`, `unshift`), is refused like the
637        // buffer is.
638        let slots = self.slots();
639        let old = &slots[..self.len().min(slots.len())];
640        let mut items = Vec::new();
641        if items.try_reserve_exact(old.len() + 1).is_err() {
642            return Err(std::alloc::Layout::array::<Value>(old.len() + 1).unwrap_or(std::alloc::Layout::new::<u8>()));
643        }
644        items.extend(old.iter().map(|x| Value(x.load(Ordering::Acquire))));
645        let r = f(&mut items);
646        self.try_replace(&items, access)?;
647        Ok(r)
648    }
649
650    /// Moves the items to a buffer with room for `need`, or returns the layout the allocator
651    /// refused, leaving the list as it was.
652    unsafe fn grow(&self, need: usize, access: Access) -> Result<(), std::alloc::Layout> {
653        let n = self.len.load(Ordering::Relaxed);
654        let mut cap = need.max(self.capacity() * 2).max(4);
655        if self.is_inline() {
656            // Leaving the odd inline capacity: continue with powers of two, as a list that
657            // started empty out of line would.
658            cap = cap.next_power_of_two();
659        }
660        let old_buf = self.buf.load(Ordering::Relaxed);
661        let old = &WordBuf::slots(old_buf)[..n];
662        let buf = WordBuf::try_alloc_from(cap, old.iter().map(|s| s.load(Ordering::Relaxed)), Value::NIL.0)?;
663        // The items keep their indices, so they keep their written range. Items that were
664        // inline count as written throughout (the new buffer's initial range).
665        if old_buf != self.inline_addr() {
666            WordBuf::copy_written(old_buf, buf);
667        }
668        self.swap_buf(buf, access);
669        Ok(())
670    }
671
672    unsafe fn swap_buf(&self, buf: *mut WordBuf, access: Access) {
673        let old = self.buf.swap(buf, Ordering::AcqRel);
674        // The inline buffer is part of the object: racing readers may still read it, and it
675        // is freed with the list.
676        if old != self.inline_addr() {
677            crate::buf::release(old, access);
678        }
679    }
680}
681
682#[repr(C)]
683pub struct DictObj {
684    pub header: Header,
685    pub table: DictTable,
686}
687
688impl DictObj {
689    /// The table. Reads are safe under any race (see [`DictTable`]); writes need write access.
690    #[inline(always)]
691    pub fn table(&self) -> &DictTable {
692        &self.table
693    }
694}
695
696#[repr(C)]
697pub struct TupleObj {
698    pub header: Header,
699    pub len: usize,
700    elems: [Value; 0],
701}
702
703impl TupleObj {
704    pub fn elems(&self) -> &[Value] {
705        unsafe { std::slice::from_raw_parts(self.elems.as_ptr(), self.len) }
706    }
707    pub(crate) fn elems_mut(&mut self) -> *mut Value {
708        self.elems.as_mut_ptr()
709    }
710    pub fn size_for(n: usize) -> usize {
711        std::mem::size_of::<TupleObj>() + 8 * n
712    }
713}
714
715#[repr(C)]
716pub struct ClosureObj {
717    pub header: Header,
718    pub func: u32,
719    pub ncaps: u32,
720    caps: [Value; 0],
721}
722
723impl ClosureObj {
724    pub fn caps(&self) -> &[Value] {
725        unsafe { std::slice::from_raw_parts(self.caps.as_ptr(), self.ncaps as usize) }
726    }
727    pub(crate) fn caps_mut(&mut self) -> *mut Value {
728        self.caps.as_mut_ptr()
729    }
730    pub fn size_for(n: usize) -> usize {
731        std::mem::size_of::<ClosureObj>() + 8 * n
732    }
733}
734
735#[repr(C)]
736pub struct CellObj {
737    pub header: Header,
738    pub value: AtomicU64,
739}
740
741#[repr(C)]
742pub struct StringBuilderObj {
743    pub header: Header,
744    pub buf: UnsafeCell<String>,
745}
746
747#[repr(C)]
748pub struct MutexObj {
749    pub header: Header,
750    pub raw: parking_lot::ReentrantMutex<()>,
751    pub value: AtomicU64,
752}
753
754#[repr(C)]
755pub struct AtomicObj {
756    pub header: Header,
757    pub value: AtomicU64,
758}
759
760pub struct ChannelInner {
761    pub queue: parking_lot::Mutex<ChannelState>,
762    pub not_empty: parking_lot::Condvar,
763    pub not_full: parking_lot::Condvar,
764    pub capacity: usize,
765    /// Wakers of fibers waiting to send or receive; all are woken on any change. Registered
766    /// while holding `queue`, so a change made after the check cannot be missed.
767    pub waiters: parking_lot::Mutex<Vec<Wake>>,
768}
769
770/// Wakes a parked fiber (see the standard library's fiber scheduler).
771pub type Wake = std::sync::Arc<dyn Fn() + Send + Sync>;
772
773impl ChannelInner {
774    /// Wakes every fiber waiting on the channel.
775    pub fn wake_fibers(&self) {
776        let ws = std::mem::take(&mut *self.waiters.lock());
777        for w in ws {
778            w();
779        }
780    }
781}
782
783pub struct ChannelState {
784    pub items: std::collections::VecDeque<Value>,
785    pub closed: bool,
786}
787
788#[repr(C)]
789pub struct ChannelObj {
790    pub header: Header,
791    pub inner: Box<ChannelInner>,
792}
793
794/// Outcome of a finished thread: its value or its panic message.
795pub enum ThreadOutcome {
796    Running,
797    Done(Value),
798    Panicked(String),
799    Joined,
800}
801
802pub struct ThreadInner {
803    pub outcome: parking_lot::Mutex<ThreadOutcome>,
804    pub done: parking_lot::Condvar,
805    pub handle: parking_lot::Mutex<Option<std::thread::JoinHandle<()>>>,
806    /// Thread id of the running thread (for transferring its result).
807    pub tid: AtomicU64,
808}
809
810#[repr(C)]
811pub struct ThreadObj {
812    pub header: Header,
813    pub inner: std::sync::Arc<ThreadInner>,
814}
815
816pub trait NativeData: std::any::Any + Send + Sync {
817    fn as_any(&self) -> &dyn std::any::Any;
818}
819impl<T: std::any::Any + Send + Sync> NativeData for T {
820    fn as_any(&self) -> &dyn std::any::Any {
821        self
822    }
823}
824
825#[repr(C)]
826pub struct NativeObj {
827    pub header: Header,
828    pub data: Box<dyn NativeData>,
829}
830
831// ---------------------------------------------------------------------------------------------
832// GC hooks per object kind.
833
834unsafe fn trace_nothing(_: *const Header, _: &mut Tracer) {}
835
836unsafe fn trace_instance(h: *const Header, t: &mut Tracer) {
837    let o = &*(h as *const InstanceObj);
838    for i in 0..heap_class(h).nfields() {
839        mark_value(t, o.get(i));
840    }
841}
842unsafe fn size_instance(h: *const Header) -> usize {
843    InstanceObj::size_for(heap_class(h).nfields())
844}
845
846unsafe fn size_str(h: *const Header) -> usize {
847    (*(h as *const StrObj)).alloc_size()
848}
849
850unsafe fn fin_bigint(h: *mut Header) {
851    std::ptr::drop_in_place(&mut (*(h as *mut BigIntObj)).value);
852}
853unsafe fn size_bigint(h: *const Header) -> usize {
854    std::mem::size_of::<BigIntObj>() + (*(h as *const BigIntObj)).value.bits() as usize / 8
855}
856
857unsafe fn trace_list(h: *const Header, t: &mut Tracer) {
858    // No thread is writing the list (the world is stopped, or a minor collection of its owner
859    // runs); slots past the length are nil.
860    let l = &*(h as *const ListObj);
861    let slots = WordBuf::slots(l.buf.load(Ordering::Relaxed));
862    for s in &slots[..l.len.load(Ordering::Relaxed).min(slots.len())] {
863        mark_value(t, Value(s.load(Ordering::Relaxed)));
864    }
865}
866/// A minor collection of the owner, for an old list it wrote since the last one: only the slots
867/// recorded as written.
868unsafe fn trace_list_written(h: *const Header, t: &mut Tracer) {
869    let l = &*(h as *const ListObj);
870    let buf = l.buf.load(Ordering::Relaxed);
871    if buf == l.inline_addr() {
872        return trace_list(h, t);
873    }
874    let slots = WordBuf::slots(buf);
875    let r = WordBuf::take_written(buf);
876    let n = l.len.load(Ordering::Relaxed).min(r.end);
877    for s in &slots[r.start.min(n)..n] {
878        mark_value(t, Value(s.load(Ordering::Relaxed)));
879    }
880}
881unsafe fn fin_list(h: *mut Header) {
882    let l = &*(h as *const ListObj);
883    let buf = l.buf.load(Ordering::Relaxed);
884    if buf != l.inline_addr() {
885        WordBuf::free(buf as *mut u8);
886    }
887}
888unsafe fn size_list(h: *const Header) -> usize {
889    let l = &*(h as *const ListObj);
890    let out = if l.is_inline() { 0 } else { l.capacity() };
891    ListObj::size_for(WordBuf::cap(&l.inline)) + 8 * out
892}
893
894unsafe fn trace_dict(h: *const Header, t: &mut Tracer) {
895    (*(h as *const DictObj)).table.trace(t);
896}
897unsafe fn trace_dict_written(h: *const Header, t: &mut Tracer) {
898    (*(h as *const DictObj)).table.trace_written(t);
899}
900unsafe fn fin_dict(h: *mut Header) {
901    std::ptr::drop_in_place(&mut (*(h as *mut DictObj)).table);
902}
903unsafe fn size_dict(h: *const Header) -> usize {
904    std::mem::size_of::<DictObj>() + (*(h as *const DictObj)).table.heap_bytes()
905}
906
907unsafe fn trace_tuple(h: *const Header, t: &mut Tracer) {
908    for &v in (*(h as *const TupleObj)).elems() {
909        mark_value(t, v);
910    }
911}
912unsafe fn size_tuple(h: *const Header) -> usize {
913    TupleObj::size_for((*(h as *const TupleObj)).len)
914}
915
916unsafe fn trace_closure(h: *const Header, t: &mut Tracer) {
917    for &v in (*(h as *const ClosureObj)).caps() {
918        mark_value(t, v);
919    }
920}
921unsafe fn size_closure(h: *const Header) -> usize {
922    ClosureObj::size_for((*(h as *const ClosureObj)).ncaps as usize)
923}
924
925unsafe fn trace_cell(h: *const Header, t: &mut Tracer) {
926    mark_value(t, Value((*(h as *const CellObj)).value.load(Ordering::Relaxed)));
927}
928unsafe fn size_cell(_: *const Header) -> usize {
929    std::mem::size_of::<CellObj>()
930}
931
932unsafe fn fin_sb(h: *mut Header) {
933    std::ptr::drop_in_place((*(h as *mut StringBuilderObj)).buf.get());
934}
935unsafe fn size_sb(h: *const Header) -> usize {
936    std::mem::size_of::<StringBuilderObj>() + (*(*(h as *const StringBuilderObj)).buf.get()).capacity()
937}
938
939unsafe fn trace_mutex(h: *const Header, t: &mut Tracer) {
940    mark_value(t, Value((*(h as *const MutexObj)).value.load(Ordering::Relaxed)));
941}
942unsafe fn fin_mutex(h: *mut Header) {
943    std::ptr::drop_in_place(&mut (*(h as *mut MutexObj)).raw);
944}
945unsafe fn size_mutex(_: *const Header) -> usize {
946    std::mem::size_of::<MutexObj>()
947}
948
949unsafe fn trace_atomic(h: *const Header, t: &mut Tracer) {
950    mark_value(t, Value((*(h as *const AtomicObj)).value.load(Ordering::Relaxed)));
951}
952unsafe fn size_atomic(_: *const Header) -> usize {
953    std::mem::size_of::<AtomicObj>()
954}
955
956unsafe fn trace_channel(h: *const Header, t: &mut Tracer) {
957    // The world is stopped; no thread holds the queue lock across a safepoint.
958    let inner = &(*(h as *const ChannelObj)).inner;
959    let q = inner.queue.data_ptr();
960    for &v in (*q).items.iter() {
961        mark_value(t, v);
962    }
963}
964unsafe fn fin_channel(h: *mut Header) {
965    std::ptr::drop_in_place(&mut (*(h as *mut ChannelObj)).inner);
966}
967unsafe fn size_channel(h: *const Header) -> usize {
968    std::mem::size_of::<ChannelObj>() + 8 * (*(*(h as *const ChannelObj)).inner.queue.data_ptr()).items.capacity()
969}
970
971unsafe fn trace_thread(h: *const Header, t: &mut Tracer) {
972    let inner = &(*(h as *const ThreadObj)).inner;
973    if let ThreadOutcome::Done(v) = &*inner.outcome.data_ptr() {
974        mark_value(t, *v);
975    }
976}
977unsafe fn fin_thread(h: *mut Header) {
978    std::ptr::drop_in_place(&mut (*(h as *mut ThreadObj)).inner);
979}
980unsafe fn size_thread(_: *const Header) -> usize {
981    std::mem::size_of::<ThreadObj>() + 128
982}
983
984unsafe fn fin_native(h: *mut Header) {
985    std::ptr::drop_in_place(&mut (*(h as *mut NativeObj)).data);
986}
987unsafe fn size_native(_: *const Header) -> usize {
988    std::mem::size_of::<NativeObj>()
989}
990
991unsafe fn size_zero(_: *const Header) -> usize {
992    0
993}
994
995#[allow(clippy::type_complexity)]
996pub(crate) fn vtable_for(kind: ObjKind) -> GcVTable {
997    let (trace, finalize, size): (
998        unsafe fn(*const Header, &mut Tracer),
999        Option<unsafe fn(*mut Header)>,
1000        unsafe fn(*const Header) -> usize,
1001    ) = match kind {
1002        ObjKind::Immediate => (trace_nothing, None, size_zero),
1003        ObjKind::Instance => (trace_instance, None, size_instance),
1004        ObjKind::Str => (trace_nothing, None, size_str),
1005        ObjKind::BigInt => (trace_nothing, Some(fin_bigint), size_bigint),
1006        ObjKind::List => (trace_list, Some(fin_list), size_list),
1007        ObjKind::Dict | ObjKind::Set => (trace_dict, Some(fin_dict), size_dict),
1008        ObjKind::Tuple => (trace_tuple, None, size_tuple),
1009        ObjKind::Closure => (trace_closure, None, size_closure),
1010        ObjKind::Cell => (trace_cell, None, size_cell),
1011        ObjKind::StringBuilder => (trace_nothing, Some(fin_sb), size_sb),
1012        ObjKind::Mutex => (trace_mutex, Some(fin_mutex), size_mutex),
1013        ObjKind::Atomic => (trace_atomic, None, size_atomic),
1014        ObjKind::Channel => (trace_channel, Some(fin_channel), size_channel),
1015        ObjKind::Thread => (trace_thread, Some(fin_thread), size_thread),
1016        ObjKind::Native => (trace_nothing, Some(fin_native), size_native),
1017    };
1018    let trace_remembered: Option<unsafe fn(*const Header, &mut Tracer)> = match kind {
1019        ObjKind::List => Some(trace_list_written),
1020        ObjKind::Dict | ObjKind::Set => Some(trace_dict_written),
1021        _ => None,
1022    };
1023    GcVTable { trace, finalize, size, trace_remembered }
1024}
1025
1026// ---------------------------------------------------------------------------------------------
1027// Typed views. Callers must have checked the kind (usually statically, by the type checker).
1028
1029macro_rules! view {
1030    ($fn:ident, $ty:ty) => {
1031        /// # Safety
1032        /// `v` must be a pointer to an object of this kind.
1033        #[inline(always)]
1034        pub unsafe fn $fn<'a>(v: Value) -> &'a $ty {
1035            &*(v.as_ptr() as *const $ty)
1036        }
1037    };
1038}
1039view!(as_instance, InstanceObj);
1040view!(as_str_obj, StrObj);
1041view!(as_bigint, BigIntObj);
1042view!(as_list, ListObj);
1043view!(as_dict, DictObj);
1044view!(as_tuple, TupleObj);
1045view!(as_closure, ClosureObj);
1046view!(as_cell, CellObj);
1047view!(as_sb, StringBuilderObj);
1048view!(as_mutex, MutexObj);
1049view!(as_atomic, AtomicObj);
1050view!(as_channel, ChannelObj);
1051view!(as_thread, ThreadObj);
1052view!(as_native, NativeObj);
1053
1054/// The object kind of a value (`Immediate` for non-pointers).
1055#[inline]
1056pub fn kind_of(v: Value) -> ObjKind {
1057    if v.is_ptr() {
1058        unsafe { heap_class(v.as_ptr()).kind }
1059    } else {
1060        ObjKind::Immediate
1061    }
1062}
1063
1064/// Returns the string slice if `v` is a string.
1065#[inline]
1066pub fn str_of<'a>(v: Value) -> Option<&'a str> {
1067    if kind_of(v) == ObjKind::Str {
1068        Some(unsafe { as_str_obj(v) }.as_str())
1069    } else {
1070        None
1071    }
1072}