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, 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#[repr(C)]
65pub struct StrObj {
66    pub header: Header,
67    pub len: usize,
68    /// Cached hash; 0 means not yet computed.
69    pub hash: AtomicU64,
70    /// Cached char count plus one; 0 means not yet computed.
71    pub chars: AtomicU64,
72    data: [u8; 0],
73}
74
75impl StrObj {
76    pub const BASE: usize = std::mem::size_of::<StrObj>();
77    pub fn as_str(&self) -> &str {
78        // SAFETY: strings are created from valid UTF-8 and never mutated.
79        unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.data.as_ptr(), self.len)) }
80    }
81    pub(crate) fn data_ptr(&mut self) -> *mut u8 {
82        self.data.as_mut_ptr()
83    }
84    pub fn char_count(&self) -> usize {
85        let c = self.chars.load(Ordering::Relaxed);
86        if c != 0 {
87            return (c - 1) as usize;
88        }
89        let s = self.as_str();
90        let n = if s.is_ascii() { s.len() } else { s.chars().count() };
91        self.chars.store(n as u64 + 1, Ordering::Relaxed);
92        n
93    }
94    pub fn hash_code(&self) -> u64 {
95        let h = self.hash.load(Ordering::Relaxed);
96        if h != 0 {
97            return h;
98        }
99        let mut h = crate::ops::hash_bytes(self.as_str().as_bytes());
100        if h == 0 {
101            h = 1;
102        }
103        self.hash.store(h, Ordering::Relaxed);
104        h
105    }
106}
107
108#[repr(C)]
109pub struct BigIntObj {
110    pub header: Header,
111    pub value: BigInt,
112}
113
114/// A list: header, length and a pointer to a backing array.
115///
116/// Reads never lock, even on shared lists. The length and buffer pointer are atomic words, and a
117/// reader bounds-checks against the buffer it loaded, so it sees some consistent-enough state and
118/// never reads out of bounds. A writer (the owner, or a thread holding a shared list's lock) that
119/// outgrows the buffer copies it into a new one and swaps the pointer; the old buffer is retired
120/// to the collector, which frees it only once the world is stopped and no reader can hold it.
121#[repr(C)]
122pub struct ListObj {
123    pub header: Header,
124    len: AtomicUsize,
125    buf: AtomicPtr<WordBuf>,
126}
127
128impl ListObj {
129    /// # Safety
130    /// `p` must point to freshly allocated, unpublished list memory.
131    pub(crate) unsafe fn init(p: *mut ListObj, items: Vec<Value>) {
132        let buf = WordBuf::alloc(items.capacity(), Value::NIL.0);
133        for (s, v) in WordBuf::slots(buf).iter().zip(&items) {
134            s.store(v.0, Ordering::Relaxed);
135        }
136        std::ptr::addr_of_mut!((*p).len).write(AtomicUsize::new(items.len()));
137        std::ptr::addr_of_mut!((*p).buf).write(AtomicPtr::new(buf));
138    }
139
140    #[inline(always)]
141    pub fn len(&self) -> usize {
142        self.len.load(Ordering::Acquire)
143    }
144
145    #[inline(always)]
146    pub fn is_empty(&self) -> bool {
147        self.len() == 0
148    }
149
150    #[inline(always)]
151    fn slots(&self) -> &[AtomicU64] {
152        unsafe { WordBuf::slots(self.buf.load(Ordering::Acquire)) }
153    }
154
155    /// The item at `i`, if in bounds. Safe against concurrent writers; the caller must still have
156    /// passed the ownership read check.
157    #[inline(always)]
158    pub fn get(&self, i: usize) -> Option<Value> {
159        if i >= self.len() {
160            return None;
161        }
162        self.slots().get(i).map(|x| Value(x.load(Ordering::Acquire)))
163    }
164
165    /// As [`ListObj::get`] for a list owned by this thread or frozen, where nothing races.
166    #[inline(always)]
167    pub fn get_local(&self, i: usize) -> Option<Value> {
168        if i >= self.len.load(Ordering::Relaxed) {
169            return None;
170        }
171        let slots = unsafe { WordBuf::slots(self.buf.load(Ordering::Relaxed)) };
172        slots.get(i).map(|x| Value(x.load(Ordering::Relaxed)))
173    }
174
175    /// A snapshot of the items.
176    pub fn to_vec(&self) -> Vec<Value> {
177        let n = self.len();
178        let slots = self.slots();
179        slots[..n.min(slots.len())].iter().map(|x| Value(x.load(Ordering::Acquire))).collect()
180    }
181
182    pub fn capacity(&self) -> usize {
183        unsafe { WordBuf::cap(self.buf.load(Ordering::Acquire)) }
184    }
185
186    // Writers. Each requires write access per the ownership rules: the owning thread, or the
187    // holder of a shared list's lock. `access` says which, so replaced buffers are freed at once
188    // when no other thread can be reading them.
189
190    /// # Safety
191    /// See above.
192    pub unsafe fn set(&self, i: usize, v: Value) -> bool {
193        if i >= self.len.load(Ordering::Relaxed) {
194            return false;
195        }
196        self.slots()[i].store(v.0, Ordering::Release);
197        true
198    }
199
200    /// # Safety
201    /// See above.
202    pub unsafe fn push(&self, v: Value, access: Access) {
203        let n = self.len.load(Ordering::Relaxed);
204        if n == self.capacity() {
205            self.grow(n + 1, access);
206        }
207        self.slots()[n].store(v.0, Ordering::Release);
208        self.len.store(n + 1, Ordering::Release);
209    }
210
211    /// # Safety
212    /// See above.
213    pub unsafe fn pop(&self) -> Option<Value> {
214        let n = self.len.load(Ordering::Relaxed);
215        if n == 0 {
216            return None;
217        }
218        let slot = &self.slots()[n - 1];
219        let v = Value(slot.load(Ordering::Relaxed));
220        self.len.store(n - 1, Ordering::Release);
221        slot.store(Value::NIL.0, Ordering::Release);
222        Some(v)
223    }
224
225    /// Replaces the contents with `items`, reusing the buffer when it is large enough.
226    ///
227    /// # Safety
228    /// See above.
229    pub unsafe fn replace(&self, items: &[Value], access: Access) {
230        let old_len = self.len.load(Ordering::Relaxed);
231        let n = items.len();
232        if n > self.capacity() {
233            let buf = WordBuf::alloc(n.max(4), Value::NIL.0);
234            let slots = WordBuf::slots(buf);
235            for (s, v) in slots.iter().zip(items) {
236                s.store(v.0, Ordering::Relaxed);
237            }
238            self.swap_buf(buf, access);
239            self.len.store(n, Ordering::Release);
240            return;
241        }
242        let slots = self.slots();
243        if n < old_len {
244            self.len.store(n, Ordering::Release);
245        }
246        for (s, v) in slots.iter().zip(items) {
247            s.store(v.0, Ordering::Release);
248        }
249        for s in &slots[n..old_len.max(n)] {
250            s.store(Value::NIL.0, Ordering::Release);
251        }
252        if n >= old_len {
253            self.len.store(n, Ordering::Release);
254        }
255    }
256
257    /// Runs `f` on a copy of the items and writes the result back.
258    ///
259    /// # Safety
260    /// See above.
261    pub unsafe fn modify<R>(&self, access: Access, f: impl FnOnce(&mut Vec<Value>) -> R) -> R {
262        let mut items = self.to_vec();
263        let r = f(&mut items);
264        self.replace(&items, access);
265        r
266    }
267
268    unsafe fn grow(&self, need: usize, access: Access) {
269        let n = self.len.load(Ordering::Relaxed);
270        let cap = need.max(self.capacity() * 2).max(4);
271        let buf = WordBuf::alloc(cap, Value::NIL.0);
272        let new = WordBuf::slots(buf);
273        for (d, s) in new.iter().zip(&self.slots()[..n]) {
274            d.store(s.load(Ordering::Relaxed), Ordering::Relaxed);
275        }
276        self.swap_buf(buf, access);
277    }
278
279    unsafe fn swap_buf(&self, buf: *mut WordBuf, access: Access) {
280        let old = self.buf.swap(buf, Ordering::AcqRel);
281        crate::buf::release(old, access);
282    }
283}
284
285#[repr(C)]
286pub struct DictObj {
287    pub header: Header,
288    pub table: DictTable,
289}
290
291impl DictObj {
292    /// The table. Reads are safe under any race (see [`DictTable`]); writes need write access.
293    #[inline(always)]
294    pub fn table(&self) -> &DictTable {
295        &self.table
296    }
297}
298
299#[repr(C)]
300pub struct TupleObj {
301    pub header: Header,
302    pub len: usize,
303    elems: [Value; 0],
304}
305
306impl TupleObj {
307    pub fn elems(&self) -> &[Value] {
308        unsafe { std::slice::from_raw_parts(self.elems.as_ptr(), self.len) }
309    }
310    pub(crate) fn elems_mut(&mut self) -> *mut Value {
311        self.elems.as_mut_ptr()
312    }
313    pub fn size_for(n: usize) -> usize {
314        std::mem::size_of::<TupleObj>() + 8 * n
315    }
316}
317
318#[repr(C)]
319pub struct ClosureObj {
320    pub header: Header,
321    pub func: u32,
322    pub ncaps: u32,
323    caps: [Value; 0],
324}
325
326impl ClosureObj {
327    pub fn caps(&self) -> &[Value] {
328        unsafe { std::slice::from_raw_parts(self.caps.as_ptr(), self.ncaps as usize) }
329    }
330    pub(crate) fn caps_mut(&mut self) -> *mut Value {
331        self.caps.as_mut_ptr()
332    }
333    pub fn size_for(n: usize) -> usize {
334        std::mem::size_of::<ClosureObj>() + 8 * n
335    }
336}
337
338#[repr(C)]
339pub struct CellObj {
340    pub header: Header,
341    pub value: AtomicU64,
342}
343
344#[repr(C)]
345pub struct StringBuilderObj {
346    pub header: Header,
347    pub buf: UnsafeCell<String>,
348}
349
350#[repr(C)]
351pub struct MutexObj {
352    pub header: Header,
353    pub raw: parking_lot::ReentrantMutex<()>,
354    pub value: AtomicU64,
355}
356
357#[repr(C)]
358pub struct AtomicObj {
359    pub header: Header,
360    pub value: AtomicU64,
361}
362
363pub struct ChannelInner {
364    pub queue: parking_lot::Mutex<ChannelState>,
365    pub not_empty: parking_lot::Condvar,
366    pub not_full: parking_lot::Condvar,
367    pub capacity: usize,
368    /// Wakers of fibers waiting to send or receive; all are woken on any change. Registered
369    /// while holding `queue`, so a change made after the check cannot be missed.
370    pub waiters: parking_lot::Mutex<Vec<Wake>>,
371}
372
373/// Wakes a parked fiber (see the standard library's fiber scheduler).
374pub type Wake = std::sync::Arc<dyn Fn() + Send + Sync>;
375
376impl ChannelInner {
377    /// Wakes every fiber waiting on the channel.
378    pub fn wake_fibers(&self) {
379        let ws = std::mem::take(&mut *self.waiters.lock());
380        for w in ws {
381            w();
382        }
383    }
384}
385
386pub struct ChannelState {
387    pub items: std::collections::VecDeque<Value>,
388    pub closed: bool,
389}
390
391#[repr(C)]
392pub struct ChannelObj {
393    pub header: Header,
394    pub inner: Box<ChannelInner>,
395}
396
397/// Outcome of a finished thread: its value or its panic message.
398pub enum ThreadOutcome {
399    Running,
400    Done(Value),
401    Panicked(String),
402    Joined,
403}
404
405pub struct ThreadInner {
406    pub outcome: parking_lot::Mutex<ThreadOutcome>,
407    pub done: parking_lot::Condvar,
408    pub handle: parking_lot::Mutex<Option<std::thread::JoinHandle<()>>>,
409    /// Thread id of the running thread (for transferring its result).
410    pub tid: AtomicU64,
411}
412
413#[repr(C)]
414pub struct ThreadObj {
415    pub header: Header,
416    pub inner: std::sync::Arc<ThreadInner>,
417}
418
419pub trait NativeData: std::any::Any + Send + Sync {
420    fn as_any(&self) -> &dyn std::any::Any;
421}
422impl<T: std::any::Any + Send + Sync> NativeData for T {
423    fn as_any(&self) -> &dyn std::any::Any {
424        self
425    }
426}
427
428#[repr(C)]
429pub struct NativeObj {
430    pub header: Header,
431    pub data: Box<dyn NativeData>,
432}
433
434// ---------------------------------------------------------------------------------------------
435// GC hooks per object kind.
436
437unsafe fn trace_nothing(_: *const Header, _: &mut Tracer) {}
438
439unsafe fn trace_instance(h: *const Header, t: &mut Tracer) {
440    let o = &*(h as *const InstanceObj);
441    for i in 0..heap_class(h).nfields() {
442        mark_value(t, o.get(i));
443    }
444}
445unsafe fn size_instance(h: *const Header) -> usize {
446    InstanceObj::size_for(heap_class(h).nfields())
447}
448
449unsafe fn size_str(h: *const Header) -> usize {
450    StrObj::BASE + (*(h as *const StrObj)).len
451}
452
453unsafe fn fin_bigint(h: *mut Header) {
454    std::ptr::drop_in_place(&mut (*(h as *mut BigIntObj)).value);
455}
456unsafe fn size_bigint(h: *const Header) -> usize {
457    std::mem::size_of::<BigIntObj>() + (*(h as *const BigIntObj)).value.bits() as usize / 8
458}
459
460unsafe fn trace_list(h: *const Header, t: &mut Tracer) {
461    // The world is stopped; slots past the length are nil.
462    for s in (*(h as *const ListObj)).slots() {
463        mark_value(t, Value(s.load(Ordering::Relaxed)));
464    }
465}
466unsafe fn fin_list(h: *mut Header) {
467    WordBuf::free(*(*(h as *mut ListObj)).buf.get_mut() as *mut u8);
468}
469unsafe fn size_list(h: *const Header) -> usize {
470    std::mem::size_of::<ListObj>() + 8 * (*(h as *const ListObj)).capacity()
471}
472
473unsafe fn trace_dict(h: *const Header, t: &mut Tracer) {
474    (*(h as *const DictObj)).table.trace(t);
475}
476unsafe fn fin_dict(h: *mut Header) {
477    std::ptr::drop_in_place(&mut (*(h as *mut DictObj)).table);
478}
479unsafe fn size_dict(h: *const Header) -> usize {
480    std::mem::size_of::<DictObj>() + (*(h as *const DictObj)).table.heap_bytes()
481}
482
483unsafe fn trace_tuple(h: *const Header, t: &mut Tracer) {
484    for &v in (*(h as *const TupleObj)).elems() {
485        mark_value(t, v);
486    }
487}
488unsafe fn size_tuple(h: *const Header) -> usize {
489    TupleObj::size_for((*(h as *const TupleObj)).len)
490}
491
492unsafe fn trace_closure(h: *const Header, t: &mut Tracer) {
493    for &v in (*(h as *const ClosureObj)).caps() {
494        mark_value(t, v);
495    }
496}
497unsafe fn size_closure(h: *const Header) -> usize {
498    ClosureObj::size_for((*(h as *const ClosureObj)).ncaps as usize)
499}
500
501unsafe fn trace_cell(h: *const Header, t: &mut Tracer) {
502    mark_value(t, Value((*(h as *const CellObj)).value.load(Ordering::Relaxed)));
503}
504unsafe fn size_cell(_: *const Header) -> usize {
505    std::mem::size_of::<CellObj>()
506}
507
508unsafe fn fin_sb(h: *mut Header) {
509    std::ptr::drop_in_place((*(h as *mut StringBuilderObj)).buf.get());
510}
511unsafe fn size_sb(h: *const Header) -> usize {
512    std::mem::size_of::<StringBuilderObj>() + (*(*(h as *const StringBuilderObj)).buf.get()).capacity()
513}
514
515unsafe fn trace_mutex(h: *const Header, t: &mut Tracer) {
516    mark_value(t, Value((*(h as *const MutexObj)).value.load(Ordering::Relaxed)));
517}
518unsafe fn fin_mutex(h: *mut Header) {
519    std::ptr::drop_in_place(&mut (*(h as *mut MutexObj)).raw);
520}
521unsafe fn size_mutex(_: *const Header) -> usize {
522    std::mem::size_of::<MutexObj>()
523}
524
525unsafe fn trace_atomic(h: *const Header, t: &mut Tracer) {
526    mark_value(t, Value((*(h as *const AtomicObj)).value.load(Ordering::Relaxed)));
527}
528unsafe fn size_atomic(_: *const Header) -> usize {
529    std::mem::size_of::<AtomicObj>()
530}
531
532unsafe fn trace_channel(h: *const Header, t: &mut Tracer) {
533    // The world is stopped; no thread holds the queue lock across a safepoint.
534    let inner = &(*(h as *const ChannelObj)).inner;
535    let q = inner.queue.data_ptr();
536    for &v in (*q).items.iter() {
537        mark_value(t, v);
538    }
539}
540unsafe fn fin_channel(h: *mut Header) {
541    std::ptr::drop_in_place(&mut (*(h as *mut ChannelObj)).inner);
542}
543unsafe fn size_channel(h: *const Header) -> usize {
544    std::mem::size_of::<ChannelObj>() + 8 * (*(*(h as *const ChannelObj)).inner.queue.data_ptr()).items.capacity()
545}
546
547unsafe fn trace_thread(h: *const Header, t: &mut Tracer) {
548    let inner = &(*(h as *const ThreadObj)).inner;
549    if let ThreadOutcome::Done(v) = &*inner.outcome.data_ptr() {
550        mark_value(t, *v);
551    }
552}
553unsafe fn fin_thread(h: *mut Header) {
554    std::ptr::drop_in_place(&mut (*(h as *mut ThreadObj)).inner);
555}
556unsafe fn size_thread(_: *const Header) -> usize {
557    std::mem::size_of::<ThreadObj>() + 128
558}
559
560unsafe fn fin_native(h: *mut Header) {
561    std::ptr::drop_in_place(&mut (*(h as *mut NativeObj)).data);
562}
563unsafe fn size_native(_: *const Header) -> usize {
564    std::mem::size_of::<NativeObj>()
565}
566
567unsafe fn size_zero(_: *const Header) -> usize {
568    0
569}
570
571#[allow(clippy::type_complexity)]
572pub(crate) fn vtable_for(kind: ObjKind) -> GcVTable {
573    let (trace, finalize, size): (
574        unsafe fn(*const Header, &mut Tracer),
575        Option<unsafe fn(*mut Header)>,
576        unsafe fn(*const Header) -> usize,
577    ) = match kind {
578        ObjKind::Immediate => (trace_nothing, None, size_zero),
579        ObjKind::Instance => (trace_instance, None, size_instance),
580        ObjKind::Str => (trace_nothing, None, size_str),
581        ObjKind::BigInt => (trace_nothing, Some(fin_bigint), size_bigint),
582        ObjKind::List => (trace_list, Some(fin_list), size_list),
583        ObjKind::Dict | ObjKind::Set => (trace_dict, Some(fin_dict), size_dict),
584        ObjKind::Tuple => (trace_tuple, None, size_tuple),
585        ObjKind::Closure => (trace_closure, None, size_closure),
586        ObjKind::Cell => (trace_cell, None, size_cell),
587        ObjKind::StringBuilder => (trace_nothing, Some(fin_sb), size_sb),
588        ObjKind::Mutex => (trace_mutex, Some(fin_mutex), size_mutex),
589        ObjKind::Atomic => (trace_atomic, None, size_atomic),
590        ObjKind::Channel => (trace_channel, Some(fin_channel), size_channel),
591        ObjKind::Thread => (trace_thread, Some(fin_thread), size_thread),
592        ObjKind::Native => (trace_nothing, Some(fin_native), size_native),
593    };
594    GcVTable { trace, finalize, size }
595}
596
597// ---------------------------------------------------------------------------------------------
598// Typed views. Callers must have checked the kind (usually statically, by the type checker).
599
600macro_rules! view {
601    ($fn:ident, $ty:ty) => {
602        /// # Safety
603        /// `v` must be a pointer to an object of this kind.
604        #[inline(always)]
605        pub unsafe fn $fn<'a>(v: Value) -> &'a $ty {
606            &*(v.as_ptr() as *const $ty)
607        }
608    };
609}
610view!(as_instance, InstanceObj);
611view!(as_str_obj, StrObj);
612view!(as_bigint, BigIntObj);
613view!(as_list, ListObj);
614view!(as_dict, DictObj);
615view!(as_tuple, TupleObj);
616view!(as_closure, ClosureObj);
617view!(as_cell, CellObj);
618view!(as_sb, StringBuilderObj);
619view!(as_mutex, MutexObj);
620view!(as_atomic, AtomicObj);
621view!(as_channel, ChannelObj);
622view!(as_thread, ThreadObj);
623view!(as_native, NativeObj);
624
625/// The object kind of a value (`Immediate` for non-pointers).
626#[inline]
627pub fn kind_of(v: Value) -> ObjKind {
628    if v.is_ptr() {
629        unsafe { heap_class(v.as_ptr()).kind }
630    } else {
631        ObjKind::Immediate
632    }
633}
634
635/// Returns the string slice if `v` is a string.
636#[inline]
637pub fn str_of<'a>(v: Value) -> Option<&'a str> {
638    if kind_of(v) == ObjKind::Str {
639        Some(unsafe { as_str_obj(v) }.as_str())
640    } else {
641        None
642    }
643}