Skip to main content

catallaxy_runtime/
mutator.rs

1//! Per-thread runtime context: identity, allocation, and the handle stack that roots values held
2//! by native code across calls back into the interpreter.
3
4use std::sync::atomic::AtomicU64;
5use std::sync::Arc;
6
7// SAFETY: a Mutator is moved to the thread it serves before that thread runs.
8unsafe impl Send for Mutator {}
9
10use catallaxy_gc::{Header, LocalHeap, ObjState, ThreadRecord};
11use num_bigint::BigInt;
12use num_traits::ToPrimitive;
13
14use crate::class::{Class, BUILTINS};
15use crate::dict::DictTable;
16use crate::object::*;
17use crate::value::{fits_small, Value};
18
19/// A recoverable interruption of execution.
20#[derive(Debug)]
21pub enum Trap {
22    /// A panic: unwinds the current thread.
23    Panic(String),
24    /// Non-local control flow (break/return out of a block) unwinding to an interpreter frame.
25    Unwind { frame: u64, kind: u8, value: Value },
26    /// `exit(code)`.
27    Exit(i32),
28}
29
30pub type RtResult<T> = Result<T, Trap>;
31
32pub fn panic<T>(msg: impl Into<String>) -> RtResult<T> {
33    Err(Trap::Panic(msg.into()))
34}
35
36pub struct Mutator {
37    pub tid: u16,
38    pub rec: Arc<ThreadRecord>,
39    /// The thread's allocator. Shared with the collector (which retires its pages while the
40    /// thread is stopped), so it is only ever accessed through this raw pointer.
41    heap: *mut LocalHeap,
42    /// Values rooted by native code. Traced as roots by the collector, which reads them while
43    /// this thread is stopped, so they live in a separate allocation reached by raw pointer.
44    handles: *mut Vec<Value>,
45}
46
47/// An index into the handle stack, valid inside the [`Mutator::scope`] that created it.
48#[derive(Copy, Clone)]
49pub struct Handle(usize);
50
51impl Mutator {
52    /// Creates the context for an already-registered thread.
53    pub fn new(rec: Arc<ThreadRecord>) -> Self {
54        let heap = Box::into_raw(Box::new(LocalHeap::new(rec.tid)));
55        let handles = Box::into_raw(Box::new(Vec::new()));
56        Mutator { tid: rec.tid, rec, heap, handles }
57    }
58
59    /// Whether new objects start young (see `catallaxy_gc::LocalHeap::set_young_alloc`).
60    pub fn set_young_alloc(&mut self, on: bool) {
61        unsafe { (*self.heap).set_young_alloc(on) }
62    }
63
64    /// A second context for the same thread (a fiber): same heap and thread record, its own
65    /// handle stack. Release it with [`Mutator::free_handles`].
66    pub fn sharing(other: &Mutator) -> Self {
67        Mutator {
68            tid: other.tid,
69            rec: other.rec.clone(),
70            heap: other.heap,
71            handles: Box::into_raw(Box::new(Vec::new())),
72        }
73    }
74
75    /// Frees the handle stack of a context made by [`Mutator::sharing`].
76    ///
77    /// # Safety
78    /// No root source may still reference the handles.
79    pub unsafe fn free_handles(&mut self) {
80        drop(Box::from_raw(self.handles));
81        self.handles = std::ptr::null_mut();
82    }
83
84    /// Raw pointer to the allocator, for registration with the collector.
85    pub fn heap_ptr(&self) -> *mut LocalHeap {
86        self.heap
87    }
88
89    /// Returns the thread's pages to the global pool (on thread exit, in native state).
90    pub fn release_pages(&mut self) {
91        // SAFETY: the heap pointer is valid for the mutator's lifetime; the caller is in native
92        // state so the collector is not using it concurrently... except a collection may be
93        // running, which is why callers hold the thread out of `Running` via unregistration.
94        unsafe { (*self.heap).release_pages() }
95    }
96
97    // ---- handles ------------------------------------------------------------------------
98
99    /// Runs `f` with a handle scope; handles rooted inside are released afterwards.
100    pub fn scope<R>(&mut self, f: impl FnOnce(&mut Mutator) -> R) -> R {
101        let base = self.handles_len();
102        let r = f(self);
103        // SAFETY: only this thread mutates its handles, never during a collection.
104        unsafe { (*self.handles).truncate(base) };
105        r
106    }
107    pub fn root(&mut self, v: Value) -> Handle {
108        unsafe {
109            (*self.handles).push(v);
110            Handle((*self.handles).len() - 1)
111        }
112    }
113    pub fn get(&self, h: Handle) -> Value {
114        unsafe { (&(*self.handles))[h.0] }
115    }
116    pub fn set(&mut self, h: Handle, v: Value) {
117        unsafe { (&mut (*self.handles))[h.0] = v };
118    }
119    fn handles_len(&self) -> usize {
120        unsafe { (*self.handles).len() }
121    }
122    /// The current depth of the handle stack, for [`Mutator::release_handles`]. Lets foreign
123    /// code (the C API) manage scopes explicitly.
124    pub fn handle_mark(&self) -> usize {
125        self.handles_len()
126    }
127    /// Releases every handle rooted since `mark`.
128    pub fn release_handles(&mut self, mark: usize) {
129        unsafe { (*self.handles).truncate(mark) };
130    }
131    /// Root values for the GC (called while this thread is stopped).
132    pub fn handle_values(&self) -> &[Value] {
133        unsafe { &*self.handles }
134    }
135    /// Pointer to the handle stack, for root sources that must not borrow the mutator.
136    pub fn handles_ptr(&self) -> *const Vec<Value> {
137        self.handles
138    }
139
140    // ---- allocation ---------------------------------------------------------------------
141
142    #[inline]
143    pub fn alloc_raw(&mut self, size: usize, cls: &'static Class) -> *mut Header {
144        // SAFETY: only this (running) thread touches its heap outside of collections.
145        unsafe { (*self.heap).alloc(size, &cls.gc as *const _) }
146    }
147
148    pub fn alloc_instance(&mut self, cls: &'static Class) -> Value {
149        let n = cls.nfields();
150        let h = self.alloc_raw(InstanceObj::size_for(n), cls);
151        let inst = unsafe { &*(h as *const InstanceObj) };
152        for i in 0..n {
153            inst.set(i, Value::NIL);
154        }
155        Value::from_ptr(h)
156    }
157
158    pub fn alloc_str(&mut self, s: &str) -> Value {
159        let h = self.alloc_raw(StrObj::BASE + s.len(), BUILTINS.string);
160        unsafe {
161            let o = &mut *(h as *mut StrObj);
162            o.len = s.len();
163            std::ptr::copy_nonoverlapping(s.as_ptr(), o.data_ptr(), s.len());
164            (*h).set_state(ObjState::Frozen);
165        }
166        Value::from_ptr(h)
167    }
168
169    /// Allocates a big integer, or returns a small int if it fits.
170    pub fn int_from_big(&mut self, b: BigInt) -> Value {
171        if let Some(i) = b.to_i64() {
172            if fits_small(i) {
173                return Value::small_int(i);
174            }
175        }
176        let h = self.alloc_raw(std::mem::size_of::<BigIntObj>(), BUILTINS.bigint);
177        unsafe {
178            std::ptr::write(&mut (*(h as *mut BigIntObj)).value, b);
179            (*h).set_state(ObjState::Frozen);
180        }
181        Value::from_ptr(h)
182    }
183
184    #[inline]
185    pub fn int(&mut self, i: i64) -> Value {
186        if fits_small(i) {
187            Value::small_int(i)
188        } else {
189            self.int_from_big(BigInt::from(i))
190        }
191    }
192
193    pub fn alloc_list(&mut self, items: Vec<Value>) -> Value {
194        let h = self.alloc_raw(std::mem::size_of::<ListObj>(), BUILTINS.list);
195        unsafe { ListObj::init(h as *mut ListObj, items) };
196        Value::from_ptr(h)
197    }
198
199    pub fn alloc_dict(&mut self, table: DictTable) -> Value {
200        self.alloc_table(table, BUILTINS.dict)
201    }
202
203    pub fn alloc_set(&mut self, table: DictTable) -> Value {
204        self.alloc_table(table, BUILTINS.set)
205    }
206
207    fn alloc_table(&mut self, table: DictTable, cls: &'static Class) -> Value {
208        let h = self.alloc_raw(std::mem::size_of::<DictObj>(), cls);
209        unsafe { std::ptr::write(std::ptr::addr_of_mut!((*(h as *mut DictObj)).table), table) };
210        Value::from_ptr(h)
211    }
212
213    pub fn alloc_tuple(&mut self, items: &[Value]) -> Value {
214        let h = self.alloc_raw(TupleObj::size_for(items.len()), BUILTINS.tuple);
215        unsafe {
216            let t = &mut *(h as *mut TupleObj);
217            t.len = items.len();
218            std::ptr::copy_nonoverlapping(items.as_ptr(), t.elems_mut(), items.len());
219        }
220        Value::from_ptr(h)
221    }
222
223    pub fn alloc_closure(&mut self, func: u32, caps: &[Value]) -> Value {
224        let h = self.alloc_raw(ClosureObj::size_for(caps.len()), BUILTINS.closure);
225        unsafe {
226            let c = &mut *(h as *mut ClosureObj);
227            c.func = func;
228            c.ncaps = caps.len() as u32;
229            std::ptr::copy_nonoverlapping(caps.as_ptr(), c.caps_mut(), caps.len());
230        }
231        Value::from_ptr(h)
232    }
233
234    pub fn alloc_cell(&mut self, v: Value) -> Value {
235        let h = self.alloc_raw(std::mem::size_of::<CellObj>(), BUILTINS.cell);
236        unsafe { (*(h as *mut CellObj)).value = AtomicU64::new(v.0) };
237        Value::from_ptr(h)
238    }
239
240    pub fn alloc_string_builder(&mut self, s: String) -> Value {
241        let h = self.alloc_raw(std::mem::size_of::<StringBuilderObj>(), BUILTINS.string_builder);
242        unsafe { std::ptr::write((*(h as *mut StringBuilderObj)).buf.get(), s) };
243        Value::from_ptr(h)
244    }
245
246    pub fn alloc_mutex(&mut self, v: Value) -> Value {
247        let h = self.alloc_raw(std::mem::size_of::<MutexObj>(), BUILTINS.mutex);
248        unsafe {
249            std::ptr::write(&mut (*(h as *mut MutexObj)).raw, parking_lot::ReentrantMutex::new(()));
250            (*(h as *mut MutexObj)).value = AtomicU64::new(v.0);
251            (*h).set_state(ObjState::Shared);
252        }
253        Value::from_ptr(h)
254    }
255
256    pub fn alloc_atomic(&mut self, v: Value) -> Value {
257        let h = self.alloc_raw(std::mem::size_of::<AtomicObj>(), BUILTINS.atomic);
258        unsafe {
259            (*(h as *mut AtomicObj)).value = AtomicU64::new(v.0);
260            (*h).set_state(ObjState::Shared);
261        }
262        Value::from_ptr(h)
263    }
264
265    pub fn alloc_channel(&mut self, capacity: usize) -> Value {
266        let h = self.alloc_raw(std::mem::size_of::<ChannelObj>(), BUILTINS.channel);
267        let inner = Box::new(ChannelInner {
268            queue: parking_lot::Mutex::new(ChannelState { items: Default::default(), closed: false }),
269            not_empty: parking_lot::Condvar::new(),
270            not_full: parking_lot::Condvar::new(),
271            capacity,
272            waiters: parking_lot::Mutex::new(Vec::new()),
273        });
274        unsafe {
275            std::ptr::write(&mut (*(h as *mut ChannelObj)).inner, inner);
276            (*h).set_state(ObjState::Shared);
277        }
278        Value::from_ptr(h)
279    }
280
281    pub fn alloc_thread(&mut self) -> Value {
282        let h = self.alloc_raw(std::mem::size_of::<ThreadObj>(), BUILTINS.thread);
283        let inner = std::sync::Arc::new(ThreadInner {
284            outcome: parking_lot::Mutex::new(ThreadOutcome::Running),
285            done: parking_lot::Condvar::new(),
286            handle: parking_lot::Mutex::new(None),
287            tid: AtomicU64::new(0),
288        });
289        unsafe {
290            std::ptr::write(&mut (*(h as *mut ThreadObj)).inner, inner);
291            (*h).set_state(ObjState::Shared);
292        }
293        Value::from_ptr(h)
294    }
295
296    pub fn alloc_native(&mut self, data: Box<dyn NativeData>) -> Value {
297        let h = self.alloc_raw(std::mem::size_of::<NativeObj>(), BUILTINS.native);
298        unsafe { std::ptr::write(&mut (*(h as *mut NativeObj)).data, data) };
299        Value::from_ptr(h)
300    }
301
302    /// Allocates a frozen native handle: one whose data is immutable or synchronizes itself, so
303    /// it may be stored in frozen objects and used from any thread (`NativeHandle` in the prelude).
304    pub fn alloc_native_frozen(&mut self, data: Box<dyn NativeData>) -> Value {
305        let v = self.alloc_native(data);
306        // A leaf, like a string: no children to escape.
307        unsafe { (*v.as_ptr()).set_state(ObjState::Frozen) };
308        v
309    }
310
311    pub fn alloc_range(&mut self, first: Value, last: Value, exclusive: bool) -> Value {
312        let v = self.alloc_instance(BUILTINS.range);
313        let r = unsafe { as_instance(v) };
314        r.set(0, first);
315        r.set(1, last);
316        r.set(2, Value::bool(exclusive));
317        unsafe { (*v.as_ptr()).set_state(ObjState::Frozen) };
318        v
319    }
320}
321
322/// Memory held outside GC pages (vector buffers) counts toward the collection budget.
323pub fn note_external(bytes: usize) {
324    if bytes > 0 {
325        catallaxy_gc::stats::note_allocated(bytes);
326    }
327}
328
329/// A region in which native code can root values across calls back into the interpreter (which
330/// may collect). Handles borrow the scope, so the borrow checker keeps them from escaping it;
331/// dropping the scope releases them.
332///
333/// ```ignore
334/// let scope = HandleScope::new(&mut vm.m);
335/// let list = scope.root(make_list(scope.mutator()));
336/// vm_call_that_may_collect();
337/// let still_valid = scope.get(list);
338/// ```
339pub struct HandleScope<'m> {
340    m: *mut Mutator,
341    base: usize,
342    _borrow: std::marker::PhantomData<&'m mut Mutator>,
343}
344
345/// A rooted value, valid for the lifetime of its [`HandleScope`].
346#[derive(Copy, Clone)]
347pub struct Local<'s> {
348    index: usize,
349    _scope: std::marker::PhantomData<&'s ()>,
350}
351
352impl<'m> HandleScope<'m> {
353    pub fn new(m: &'m mut Mutator) -> Self {
354        let base = m.handles_len();
355        HandleScope { m, base, _borrow: std::marker::PhantomData }
356    }
357
358    /// A scope that does not borrow the mutator, so native code can keep calling into the VM
359    /// while it holds handles. Handles still cannot outlive the scope.
360    ///
361    /// # Safety
362    /// `m` must outlive the scope and belong to the current thread.
363    pub unsafe fn detached(m: *mut Mutator) -> HandleScope<'static> {
364        let base = (*m).handles_len();
365        HandleScope { m, base, _borrow: std::marker::PhantomData }
366    }
367
368    /// Roots `v` until the scope ends.
369    pub fn root<'s>(&'s self, v: Value) -> Local<'s> {
370        // SAFETY: the scope holds the mutator's unique borrow; handles are only read by the
371        // collector while this thread is stopped.
372        let hs = unsafe { &mut *(*self.m).handles };
373        hs.push(v);
374        Local { index: hs.len() - 1, _scope: std::marker::PhantomData }
375    }
376
377    pub fn get(&self, h: Local<'_>) -> Value {
378        unsafe { (&(*(*self.m).handles))[h.index] }
379    }
380
381    pub fn set(&self, h: Local<'_>, v: Value) {
382        unsafe { (&mut (*(*self.m).handles))[h.index] = v }
383    }
384
385    /// The mutator, for allocation. Allocation never collects, so handles stay valid.
386    #[allow(clippy::mut_from_ref)]
387    pub fn mutator(&self) -> &mut Mutator {
388        unsafe { &mut *self.m }
389    }
390}
391
392impl Drop for HandleScope<'_> {
393    fn drop(&mut self) {
394        unsafe { (*(*self.m).handles).truncate(self.base) };
395    }
396}