Skip to main content

catallaxy_runtime/
ownership.rs

1//! Ownership states at runtime: access checks, deep freeze, promotion to shared, and transfer
2//! of owned object graphs between threads.
3//!
4//! The type checker rejects most cross-thread misuse statically. These checks are the backstop for
5//! paths it cannot see (`Dyn`, erased generics, native code), and they are what guarantees memory
6//! safety: container buffers are only mutated by their owning thread, or under the object's lock
7//! when shared.
8
9use std::sync::atomic::{AtomicBool, Ordering};
10
11use catallaxy_gc::{Header, ObjState};
12
13use crate::class::{heap_class, ObjKind};
14use crate::dict::DictTable;
15use crate::mutator::{Mutator, RtResult, Trap};
16use crate::object::*;
17use crate::value::Value;
18
19/// When set, touching another thread's owned object promotes it to shared (with a warning)
20/// instead of panicking with `OwnershipError`.
21pub static PROMOTE_ON_ACCESS: AtomicBool = AtomicBool::new(false);
22
23/// Reports promotions under `--promote-on-access`. The VM installs a hook that names the site.
24static PROMOTION_HOOK: std::sync::OnceLock<fn(&str)> = std::sync::OnceLock::new();
25
26pub fn set_promotion_hook(f: fn(&str)) {
27    let _ = PROMOTION_HOOK.set(f);
28}
29
30/// How an access must be performed.
31#[derive(Copy, Clone, PartialEq, Eq, Debug)]
32pub enum Access {
33    /// Owned by this thread, or frozen (for reads): plain loads and stores.
34    Local,
35    /// Shared: take the object's lock (containers) or use acquire/release (fields).
36    Shared,
37}
38
39/// Checks that the current thread may read the object at `h`.
40#[inline(always)]
41pub fn check_read(m: &Mutator, h: &Header) -> RtResult<Access> {
42    let meta = h.meta();
43    match Header::state_of(meta) {
44        ObjState::Owned if Header::owner_of(meta) == m.tid => Ok(Access::Local),
45        ObjState::Frozen => Ok(Access::Local),
46        ObjState::Shared => Ok(Access::Shared),
47        ObjState::Owned => foreign(m, h),
48    }
49}
50
51/// Checks that the current thread may write the object at `h`. Writing into an old object this
52/// thread owns adds it to the remembered set (it may come to point at young objects).
53#[inline(always)]
54pub fn check_write(m: &Mutator, h: &Header) -> RtResult<Access> {
55    let meta = h.meta();
56    match Header::state_of(meta) {
57        ObjState::Owned if Header::owner_of(meta) == m.tid => {
58            if Header::needs_remember(meta) {
59                remember(m, h);
60            }
61            Ok(Access::Local)
62        }
63        ObjState::Shared => Ok(Access::Shared),
64        ObjState::Frozen => Err(frozen_error(h)),
65        ObjState::Owned => foreign(m, h),
66    }
67}
68
69pub fn frozen_error(h: &Header) -> Trap {
70    let cls = unsafe { heap_class(h) };
71    Trap::Panic(format!("FrozenError: cannot modify frozen {}", cls.name))
72}
73
74#[cold]
75fn foreign(m: &Mutator, h: &Header) -> RtResult<Access> {
76    let cls = unsafe { heap_class(h) };
77    if PROMOTE_ON_ACCESS.load(Ordering::Relaxed) {
78        share_deep(m, Value::from_ptr(h), true)?;
79        if let Some(hook) = PROMOTION_HOOK.get() {
80            hook(&cls.name);
81        } else {
82            eprintln!("warning: promoted {} to shared on access from thread {}", cls.name, m.tid);
83        }
84        return Ok(Access::Shared);
85    }
86    Err(Trap::Panic(format!(
87        "OwnershipError: {} owned by thread {} accessed from thread {}",
88        cls.name,
89        h.owner(),
90        m.tid
91    )))
92}
93
94/// Write barrier slow path: records `h`, an old object owned by this thread, in the remembered set.
95#[cold]
96pub fn remember(m: &Mutator, h: &Header) {
97    // SAFETY: the heap belongs to this (running) thread.
98    unsafe { (*m.heap_ptr()).remember(h) };
99}
100
101/// Makes everything reachable from `v` old that a minor collection of thread `tid` could
102/// otherwise free: young objects, found directly or through old objects `tid` owns and wrote
103/// since its last minor collection. Every path that lets another thread or a global reach `v`
104/// calls this first (sharing, freezing, moving, publishing).
105pub fn escape(v: Value, tid: u16) {
106    let Some(h) = v.header() else { return };
107    let meta = h.meta();
108    let owned_old = Header::state_of(meta) == ObjState::Owned && Header::owner_of(meta) == tid && h.is_remembered();
109    if !h.is_young() && !owned_old {
110        return; // the common case: nothing young below an old, unwritten object
111    }
112    let mut work = vec![v];
113    let mut seen_old = rustc_hash::FxHashSet::default();
114    while let Some(x) = work.pop() {
115        let Some(h) = x.header() else { continue };
116        if h.is_young() {
117            h.tenure();
118            for_each_child(x, |c| work.push(c));
119            continue;
120        }
121        let meta = h.meta();
122        if Header::state_of(meta) == ObjState::Owned
123            && Header::owner_of(meta) == tid
124            && h.is_remembered()
125            && seen_old.insert(x.0)
126        {
127            for_each_child(x, |c| work.push(c));
128        }
129    }
130}
131
132/// Calls `f` for every value directly referenced by the object `v`.
133pub fn for_each_child(v: Value, mut f: impl FnMut(Value)) {
134    let Some(h) = v.header() else { return };
135    let cls = unsafe { heap_class(h) };
136    unsafe {
137        match cls.kind {
138            ObjKind::Instance => {
139                let o = as_instance(v);
140                for i in 0..cls.nfields() {
141                    f(o.get(i));
142                }
143            }
144            ObjKind::List => {
145                for x in as_list(v).to_vec() {
146                    f(x)
147                }
148            }
149            ObjKind::Dict | ObjKind::Set => {
150                for e in as_dict(v).table().iter() {
151                    f(e.key);
152                    f(e.value);
153                }
154            }
155            ObjKind::Tuple => {
156                for &x in as_tuple(v).elems() {
157                    f(x)
158                }
159            }
160            ObjKind::Closure => {
161                for &x in as_closure(v).caps() {
162                    f(x)
163                }
164            }
165            ObjKind::Cell => f(Value(as_cell(v).value.load(Ordering::Relaxed))),
166            ObjKind::Mutex | ObjKind::Atomic => {
167                // Shareable wrappers own their contents; they are never walked into.
168            }
169            _ => {}
170        }
171    }
172}
173
174/// Deeply freezes `v`: every reachable object owned by this thread becomes frozen.
175/// Frozen and shared objects are left as they are (they never point to owned ones).
176pub fn freeze_deep(m: &Mutator, v: Value) -> RtResult<()> {
177    // Frozen objects may be shared freely, so they must be old.
178    escape(v, m.tid);
179    let mut work = vec![v];
180    while let Some(x) = work.pop() {
181        let Some(h) = x.header() else { continue };
182        let meta = h.meta();
183        match Header::state_of(meta) {
184            ObjState::Frozen | ObjState::Shared => continue,
185            ObjState::Owned if Header::owner_of(meta) != m.tid => {
186                let cls = unsafe { heap_class(h) };
187                return Err(Trap::Panic(format!(
188                    "OwnershipError: cannot freeze {} owned by thread {}",
189                    cls.name,
190                    h.owner()
191                )));
192            }
193            ObjState::Owned => {
194                let kind = unsafe { heap_class(h).kind };
195                if matches!(kind, ObjKind::StringBuilder | ObjKind::Native | ObjKind::Thread) {
196                    // Mutable native state cannot be frozen; it stays owned.
197                    continue;
198                }
199                h.set_state(ObjState::Frozen);
200                for_each_child(x, |c| work.push(c));
201            }
202        }
203    }
204    Ok(())
205}
206
207/// Deeply promotes `v` to shared. With `steal`, objects owned by other threads are promoted too
208/// (`--promote-on-access`); otherwise they are an error.
209pub fn share_deep(m: &Mutator, v: Value, steal: bool) -> RtResult<()> {
210    escape(v, m.tid);
211    let mut work = vec![v];
212    while let Some(x) = work.pop() {
213        let Some(h) = x.header() else { continue };
214        let meta = h.meta();
215        match Header::state_of(meta) {
216            ObjState::Frozen | ObjState::Shared => continue,
217            ObjState::Owned if !steal && Header::owner_of(meta) != m.tid => {
218                let cls = unsafe { heap_class(h) };
219                return Err(Trap::Panic(format!(
220                    "OwnershipError: cannot share {} owned by thread {}",
221                    cls.name,
222                    h.owner()
223                )));
224            }
225            ObjState::Owned => {
226                h.set_state(ObjState::Shared);
227                for_each_child(x, |c| work.push(c));
228            }
229        }
230    }
231    Ok(())
232}
233
234/// Moves ownership of every object reachable from `v` and owned by `from` to `to`.
235/// Objects owned by anyone else, frozen or shared are not touched.
236pub fn transfer_deep(v: Value, from: u16, to: u16) {
237    if from == to {
238        return;
239    }
240    // The receiver must not see objects the sender's next minor collection could free. Values
241    // in transit (`from` is the transit id) are already old.
242    escape(v, from);
243    let mut work = vec![v];
244    while let Some(x) = work.pop() {
245        let Some(h) = x.header() else { continue };
246        let meta = h.meta();
247        if Header::state_of(meta) == ObjState::Owned && Header::owner_of(meta) == from {
248            h.set_owned_by(to);
249            for_each_child(x, |c| work.push(c));
250        }
251    }
252}
253
254/// Owner id used for values in transit through a channel.
255pub const TRANSIT_TID: u16 = 0;
256
257/// Prepares a value to be stored into a shared container: owned objects of this thread are
258/// promoted to shared so other threads may reach them.
259#[inline]
260pub fn publish_into_shared(m: &Mutator, v: Value) -> RtResult<()> {
261    escape(v, m.tid);
262    if let Some(h) = v.header() {
263        if h.state() == ObjState::Owned {
264            share_deep(m, v, false)?;
265        }
266    }
267    Ok(())
268}
269
270/// Takes a shared object's lock. If it is contended, the thread waits in native state so a lock
271/// holder that reaches a safepoint (inside `with lock`) can still collect.
272pub fn lock_obj<'a>(m: &Mutator, h: &'a Header) -> catallaxy_gc::ObjLockGuard<'a> {
273    if !h.try_lock(m.tid) {
274        catallaxy_gc::enter_native(&m.rec);
275        h.lock(m.tid);
276        catallaxy_gc::leave_native(&m.rec);
277    }
278    catallaxy_gc::ObjLockGuard::adopt(h)
279}
280
281/// Checks that the current thread may read the list `v`. List reads never lock (see
282/// [`ListObj`]), so this is the only synchronization a list read needs. Raises a `TypeError`
283/// if `v` is not a list.
284// Not inlined: the check's error path, inlined into the interpreter loop's slow paths, costs the
285// whole loop (register allocation), and list reads off the interpreter's fast paths are calls.
286#[inline(never)]
287pub fn read_list<'a>(m: &Mutator, v: Value) -> RtResult<&'a ListObj> {
288    let l = list_of(v)?;
289    check_read(m, unsafe { &*v.as_ptr() })?;
290    Ok(l)
291}
292
293/// Runs `f` on a list with write access: directly when owned, under the lock when shared.
294/// For shared lists, values stored must first go through [`publish_into_shared`].
295/// Raises a `TypeError` if `v` is not a list.
296#[inline]
297pub fn write_list<R>(m: &Mutator, v: Value, f: impl FnOnce(&ListObj, Access) -> R) -> RtResult<R> {
298    let l = list_of(v)?;
299    with_write(m, v, |access| f(l, access))
300}
301
302/// Reads a dict or set. Owned and frozen tables are read directly; shared ones through the
303/// table's seqlock, falling back to the object's lock when writers keep overlapping. `f` may run
304/// more than once and must only read. Raises a `TypeError` if `v` is not a dict or set.
305#[inline]
306pub fn read_dict<R>(m: &Mutator, v: Value, f: impl Fn(&DictTable) -> R) -> RtResult<R> {
307    dict_of(v)?;
308    // SAFETY: checked just above.
309    unsafe { read_dict_unchecked(m, v, f) }
310}
311
312/// [`read_dict`] without the kind check, for paths that read a table repeatedly.
313///
314/// # Safety
315/// `v` must be a dict or set, checked by the caller (static types are not enough).
316#[inline]
317pub unsafe fn read_dict_unchecked<R>(m: &Mutator, v: Value, f: impl Fn(&DictTable) -> R) -> RtResult<R> {
318    let t = unsafe { as_dict(v) }.table();
319    let h = unsafe { &*v.as_ptr() };
320    match check_read(m, h)? {
321        Access::Local => Ok(f(t)),
322        Access::Shared => match t.read_optimistic(&f) {
323            Some(r) => Ok(r),
324            None => {
325                let _g = lock_obj(m, h);
326                Ok(f(t))
327            }
328        },
329    }
330}
331
332/// Runs `f` on a dict or set with write access: directly when owned, under the lock when shared.
333/// Raises a `TypeError` if `v` is not a dict or set.
334#[inline]
335pub fn write_dict<R>(m: &Mutator, v: Value, f: impl FnOnce(&DictTable, Access) -> R) -> RtResult<R> {
336    dict_of(v)?;
337    // SAFETY: checked just above.
338    unsafe { write_dict_unchecked(m, v, f) }
339}
340
341/// [`write_dict`] without the kind check.
342///
343/// # Safety
344/// `v` must be a dict or set, checked by the caller (static types are not enough).
345#[inline]
346pub unsafe fn write_dict_unchecked<R>(m: &Mutator, v: Value, f: impl FnOnce(&DictTable, Access) -> R) -> RtResult<R> {
347    with_write(m, v, |access| f(unsafe { as_dict(v) }.table(), access))
348}
349
350/// Runs `f` on a container's contents with the right synchronization for a read. `v` must be a
351/// heap object whose kind the caller has checked (with a checked view such as [`sb_of`]).
352#[inline]
353pub fn with_read<R>(m: &Mutator, v: Value, f: impl FnOnce() -> R) -> RtResult<R> {
354    debug_assert!(v.is_ptr(), "with_read on an immediate");
355    let h = unsafe { &*v.as_ptr() };
356    match check_read(m, h)? {
357        Access::Local => Ok(f()),
358        Access::Shared => {
359            let _g = lock_obj(m, h);
360            Ok(f())
361        }
362    }
363}
364
365/// Runs `f` on a container's contents with the right synchronization for a write.
366/// For shared containers, values stored must first go through [`publish_into_shared`]. `v` must
367/// be a heap object whose kind the caller has checked (with a checked view such as [`sb_of`]).
368#[inline]
369pub fn with_write<R>(m: &Mutator, v: Value, f: impl FnOnce(Access) -> R) -> RtResult<R> {
370    debug_assert!(v.is_ptr(), "with_write on an immediate");
371    let h = unsafe { &*v.as_ptr() };
372    match check_write(m, h)? {
373        Access::Local => Ok(f(Access::Local)),
374        Access::Shared => {
375            let _g = lock_obj(m, h);
376            Ok(f(Access::Shared))
377        }
378    }
379}